294 lines
8.7 KiB
Go
294 lines
8.7 KiB
Go
|
|
||
|
// readMemos provides functions to read Seekia memos and to derive blockchain addresses to timestamp memos
|
||
|
// Memos are specially formatted messages that are signed with a Seekia identity key
|
||
|
// They can be shared anywhere, and verified using the Seekia application
|
||
|
|
||
|
package readMemos
|
||
|
|
||
|
//TODO: We can relax some restrictions on the memo format.
|
||
|
// We can add translations to some of the memo terms ("Memo", "Author", "Signature")
|
||
|
// This should be able to be done later, and memos created with with the original memo format will still be valid.
|
||
|
|
||
|
import "seekia/internal/cryptocurrency/ethereumAddress"
|
||
|
import "seekia/internal/cryptocurrency/cardanoAddress"
|
||
|
|
||
|
import "seekia/internal/cryptography/blake3"
|
||
|
import "seekia/internal/cryptography/edwardsKeys"
|
||
|
import "seekia/internal/encoding"
|
||
|
import "seekia/internal/identity"
|
||
|
|
||
|
import "strings"
|
||
|
import "errors"
|
||
|
|
||
|
|
||
|
//Outputs:
|
||
|
// -bool: Memo is valid
|
||
|
// -[32]byte: Memo hash (32 bytes long blake3 hash of memo string)
|
||
|
// -[16]byte: Author identity hash
|
||
|
// -string: Memo Unarmored Contents (The contents of the memo without the header, signature, author identity key, padding, and footer)
|
||
|
// -error
|
||
|
func ReadMemo(inputMemo string)(bool, [32]byte, [16]byte, string, error){
|
||
|
|
||
|
memoHashBytes, err := blake3.GetBlake3HashAsBytes(32, []byte(inputMemo))
|
||
|
if (err != nil) {
|
||
|
// Invalid memo: Is empty.
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
memoHash := [32]byte(memoHashBytes)
|
||
|
|
||
|
memoLinesList := strings.Split(inputMemo, "\n")
|
||
|
|
||
|
if (len(memoLinesList) < 18){
|
||
|
//Invalid memo: Too short
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
topLine := memoLinesList[0]
|
||
|
|
||
|
// The decorations on this line (and the footer) can be anything.
|
||
|
// We just make sure "Seekia Memo" exists in the first line of the memo.
|
||
|
containsSeekiaMemo := strings.Contains(topLine, "Seekia Memo")
|
||
|
if (containsSeekiaMemo == false){
|
||
|
// Invalid memo: Missing Seekia Memo header.
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
if (memoLinesList[1] != "|"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
if (memoLinesList[2] != "|- Signature:"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
signaturePart1, hasPrefix := strings.CutPrefix(memoLinesList[3], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
signaturePart2, hasPrefix := strings.CutPrefix(memoLinesList[4], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
signaturePart3, hasPrefix := strings.CutPrefix(memoLinesList[5], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
signatureString := signaturePart1 + signaturePart2 + signaturePart3
|
||
|
|
||
|
signatureBytes, err := encoding.DecodeBase64StringToBytes(signatureString)
|
||
|
if (err != nil){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
if (len(signatureBytes) != 64){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
signatureArray := [64]byte(signatureBytes)
|
||
|
|
||
|
if (memoLinesList[6] != "|"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
// We create memoContent, which is the content string that will be hashed and signed by the author
|
||
|
// We trim the final line
|
||
|
finalIndex := len(memoLinesList)-1
|
||
|
memoContentList := memoLinesList[7:finalIndex]
|
||
|
memoContent := strings.Join(memoContentList, "\n")
|
||
|
memoContent += "\n"
|
||
|
|
||
|
memoContentHash, err := blake3.Get32ByteBlake3Hash([]byte(memoContent))
|
||
|
if (err != nil){ return false, [32]byte{}, [16]byte{}, "", err }
|
||
|
|
||
|
if (memoLinesList[7] != "|- Identity Key:"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
identityKeyPart1, hasPrefix := strings.CutPrefix(memoLinesList[8], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
identityKeyPart2, hasPrefix := strings.CutPrefix(memoLinesList[9], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
identityKeyHex := identityKeyPart1 + identityKeyPart2
|
||
|
|
||
|
identityKeyBytes, err := encoding.DecodeHexStringToBytes(identityKeyHex)
|
||
|
if (err != nil){
|
||
|
// Memo is malformed
|
||
|
// Invalid identity key: Not hex.
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
if (len(identityKeyBytes) != 32){
|
||
|
// Memo is malformed
|
||
|
// Invalid identity key: Invalid length.
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
identityKeyArray := [32]byte(identityKeyBytes)
|
||
|
|
||
|
if (memoLinesList[10] != "|"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
if (memoLinesList[11] != "|- Author:"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
identityHashString, hasPrefix := strings.CutPrefix(memoLinesList[12], "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
identityHash, identityType, err := identity.ReadIdentityHashString(identityHashString)
|
||
|
if (err != nil){
|
||
|
// Memo is malformed
|
||
|
// Invalid author identity hash
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
// Now we verify signature and identity hash
|
||
|
|
||
|
expectedIdentityHash, err := identity.ConvertIdentityKeyToIdentityHash(identityKeyArray, identityType)
|
||
|
if (err != nil){
|
||
|
// Memo is malformed
|
||
|
// Invalid identity key
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
if (identityHash != expectedIdentityHash){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
isValid := edwardsKeys.VerifySignature(identityKeyArray, signatureArray, memoContentHash)
|
||
|
if (isValid == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
// Signature is valid.
|
||
|
// We make sure rest of memo is well formed.
|
||
|
|
||
|
if (memoLinesList[13] != "|"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
if (memoLinesList[14] != "|- Memo:"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
if (memoLinesList[15] != "|"){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
penultimateLine := memoLinesList[finalIndex-1]
|
||
|
if (penultimateLine != "|"){
|
||
|
// Memo is malformed
|
||
|
// The second to last line must be a single pipe
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
finalLine := memoLinesList[finalIndex]
|
||
|
|
||
|
containsEndOfMemo := strings.Contains(finalLine, "End Of Memo")
|
||
|
if (containsEndOfMemo == false){
|
||
|
// Memo is malformed
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
// Now we check to make sure each line has the "|" prefix and is well formed
|
||
|
// We use this to build the unarmored memo contents
|
||
|
var unarmoredMemoContentsBuilder strings.Builder
|
||
|
|
||
|
for index, memoLine := range memoLinesList{
|
||
|
|
||
|
if (index < 16){
|
||
|
continue
|
||
|
}
|
||
|
if (index >= finalIndex-1){
|
||
|
// We have reached the penultimate line of the memo
|
||
|
// We don't include this line or the memo footer in the unarmored memo contents
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if (memoLine != "|" && memoLine != "| "){
|
||
|
|
||
|
// "|" == The line is a newline without any text content.
|
||
|
|
||
|
// "| " == This memo was created before the memo format was changed to not include whitespace before empty lines
|
||
|
|
||
|
// The line should not be empty
|
||
|
// We will add the line contents to the unarmoredContent
|
||
|
|
||
|
lineWithoutPrefix, hasPrefix := strings.CutPrefix(memoLine, "| ")
|
||
|
if (hasPrefix == false){
|
||
|
// Memo is malformed
|
||
|
// Each line must either be a single pipe, a pipe with 4 whitespace characters,
|
||
|
// or a pipe with 4 whitespace characters and content afterwards
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
if (lineWithoutPrefix == ""){
|
||
|
// Memo is malformed
|
||
|
// Each line must contain some content
|
||
|
return false, [32]byte{}, [16]byte{}, "", nil
|
||
|
}
|
||
|
|
||
|
unarmoredMemoContentsBuilder.WriteString(lineWithoutPrefix)
|
||
|
}
|
||
|
|
||
|
if (index < finalIndex-2){
|
||
|
unarmoredMemoContentsBuilder.WriteString("\n")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
unarmoredMemoContents := unarmoredMemoContentsBuilder.String()
|
||
|
|
||
|
return true, memoHash, identityHash, unarmoredMemoContents, nil
|
||
|
}
|
||
|
|
||
|
//Outputs:
|
||
|
// -string: Blockchain address that can timestamp the memo
|
||
|
// -error
|
||
|
func GetBlockchainAddressFromMemoHash(cryptocurrencyName string, memoHash [32]byte)(string, error){
|
||
|
|
||
|
if (cryptocurrencyName != "Ethereum" && cryptocurrencyName != "Cardano"){
|
||
|
return "", errors.New("GetBlockchainAddressFromMemoHash called with invalid cryptocurrencyName: " + cryptocurrencyName)
|
||
|
}
|
||
|
|
||
|
if (cryptocurrencyName == "Ethereum"){
|
||
|
|
||
|
memoEthereumAddress, err := ethereumAddress.GetMemoEthereumAddressFromMemoHash(memoHash)
|
||
|
if (err != nil) { return "", err }
|
||
|
|
||
|
return memoEthereumAddress, nil
|
||
|
}
|
||
|
|
||
|
memoCardanoAddress, err := cardanoAddress.GetMemoCardanoAddressFromMemoHash(memoHash)
|
||
|
if (err != nil) { return "", err }
|
||
|
|
||
|
return memoCardanoAddress, nil
|
||
|
}
|
||
|
|
||
|
|
||
|
|