seekia/internal/memos/readMemos/readMemos.go

294 lines
8.7 KiB
Go
Raw Normal View History

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