package walletconnect import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/binary" "encoding/hex" crypto "git.frostfs.info/TrueCloudLab/frostfs-crypto" ) const ( // saltSize is the salt size added to signed message. saltSize = 16 // signatureLen is the length of RFC6979 signature. signatureLen = 64 ) // SignedMessage contains mirrors `SignedMessage` struct from the WalletConnect API. // https://neon.coz.io/wksdk/core/modules.html#SignedMessage type SignedMessage struct { Data []byte Message []byte PublicKey []byte Salt []byte } // Sign signs message using WalletConnect API. The returned signature // contains RFC6979 signature and 16-byte salt. func Sign(p *ecdsa.PrivateKey, msg []byte) ([]byte, error) { sm, err := SignMessage(p, msg) if err != nil { return nil, err } return append(sm.Data, sm.Salt...), nil } // Verify verifies message using WalletConnect API. func Verify(p *ecdsa.PublicKey, data, sign []byte) bool { if len(sign) != signatureLen+saltSize { return false } salt := sign[signatureLen:] return VerifyMessage(p, SignedMessage{ Data: sign[:signatureLen], Message: createMessageWithSalt(data, salt), Salt: salt, }) } // SignMessage signs message with a private key and returns structure similar to // `signMessage` of the WalletConnect API. // https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L496 // https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L133 func SignMessage(p *ecdsa.PrivateKey, msg []byte) (SignedMessage, error) { var salt [saltSize]byte _, _ = rand.Read(salt[:]) msg = createMessageWithSalt(msg, salt[:]) sign, err := crypto.SignRFC6979(p, msg) if err != nil { return SignedMessage{}, err } return SignedMessage{ Data: sign, Message: msg, PublicKey: elliptic.MarshalCompressed(p.Curve, p.X, p.Y), Salt: salt[:], }, nil } // VerifyMessage verifies message with a private key and returns structure similar to // `verifyMessage` of WalletConnect API. // https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L515 // https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L147 func VerifyMessage(p *ecdsa.PublicKey, m SignedMessage) bool { if p == nil { x, y := elliptic.UnmarshalCompressed(elliptic.P256(), m.PublicKey) if x == nil || y == nil { return false } p = &ecdsa.PublicKey{ Curve: elliptic.P256(), X: x, Y: y, } } return crypto.VerifyRFC6979(p, m.Message, m.Data) == nil } func createMessageWithSalt(msg, salt []byte) []byte { // 4 byte prefix + length of the message with salt in bytes + // + salt + message + 2 byte postfix. saltedLen := hex.EncodedLen(len(salt)) + len(msg) data := make([]byte, 4+getVarIntSize(saltedLen)+saltedLen+2) n := copy(data, []byte{0x01, 0x00, 0x01, 0xf0}) // fixed prefix n += putVarUint(data[n:], uint64(saltedLen)) // salt is hex encoded, double its size n += hex.Encode(data[n:], salt[:]) // for some reason we encode salt in hex n += copy(data[n:], msg) copy(data[n:], []byte{0x00, 0x00}) return data } // Following functions are copied from github.com/nspcc-dev/neo-go/pkg/io package // to avoid having another dependency. // getVarIntSize returns the size in number of bytes of a variable integer. // Reference: https://github.com/neo-project/neo/blob/26d04a642ac5a1dd1827dabf5602767e0acba25c/src/neo/IO/Helper.cs#L131 func getVarIntSize(value int) int { var size uintptr if value < 0xFD { size = 1 // unit8 } else if value <= 0xFFFF { size = 3 // byte + uint16 } else { size = 5 // byte + uint32 } return int(size) } // putVarUint puts val in varint form to the pre-allocated buffer. func putVarUint(data []byte, val uint64) int { if val < 0xfd { data[0] = byte(val) return 1 } if val <= 0xFFFF { data[0] = byte(0xfd) binary.LittleEndian.PutUint16(data[1:], uint16(val)) return 3 } data[0] = byte(0xfe) binary.LittleEndian.PutUint32(data[1:], uint32(val)) return 5 }