// 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 }