From 100a26f49c1caa97772654085ebf5366810f1818 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 6 Apr 2021 19:08:16 -0400 Subject: [PATCH] wakuv1 envelope format --- waku/v2/node/waku_payload.go | 324 ++++++++++++++++++++++++++++------- 1 file changed, 266 insertions(+), 58 deletions(-) diff --git a/waku/v2/node/waku_payload.go b/waku/v2/node/waku_payload.go index 204bc061..77b1351b 100644 --- a/waku/v2/node/waku_payload.go +++ b/waku/v2/node/waku_payload.go @@ -5,11 +5,14 @@ import ( "crypto/cipher" "crypto/ecdsa" crand "crypto/rand" + "encoding/binary" + "fmt" mrand "math/rand" "errors" "strconv" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/status-im/go-waku/waku/v2/protocol" ) @@ -22,16 +25,122 @@ const ( None KeyKind = "None" ) -type KeyInfo struct { - Kind KeyKind - SymKey []byte - PrivKey ecdsa.PrivateKey +// The message to encode +type Payload struct { + Data []byte // Raw message payload + Padding []byte // Used to align data size, since data size alone might reveal important metainformation. + Key *KeyInfo // Contains the type of encryption to apply and the private key to use for signing the message } -// NOTICE: Extracted from status-go +// The decoded payload of a received message. +type DecodedPayload struct { + Data []byte // Decoded message payload + Padding []byte // Used to align data size, since data size alone might reveal important metainformation. + PubKey *ecdsa.PublicKey // The public key that signed the payload + Signature []byte +} + +type KeyInfo struct { + Kind KeyKind // Indicates the type of encryption to use + SymKey []byte // If the encryption is Symmetric, a Symmetric key must be specified + PubKey ecdsa.PublicKey // If the encryption is Asymmetric, the public key of the message receptor must be specified + PrivKey *ecdsa.PrivateKey // Set a privkey if the message requires a signature + +} + +func (payload Payload) Encode(version uint32) ([]byte, error) { + switch version { + case 0: + return payload.Data, nil + case 1: + data, err := payload.v1Data() + if err != nil { + return nil, err + } + + if payload.Key.PrivKey != nil { + data, err = sign(data, *payload.Key.PrivKey) + if err != nil { + return nil, err + } + } + + switch payload.Key.Kind { + case Symmetric: + encoded, err := encryptSymmetric(data, payload.Key.SymKey) + if err != nil { + return nil, errors.New("Couldn't encrypt using symmetric key") + } else { + return encoded, nil + } + case Asymmetric: + encoded, err := encryptAsymmetric(data, &payload.Key.PubKey) + if err != nil { + return nil, errors.New("Couldn't encrypt using asymmetric key") + } else { + return encoded, nil + } + case None: + return nil, errors.New("Non supported KeyKind") + } + } + return nil, errors.New("Unsupported WakuMessage version") +} + +func DecodePayload(message *protocol.WakuMessage, keyInfo *KeyInfo) (*DecodedPayload, error) { + switch *message.Version { + case uint32(0): + return &DecodedPayload{Data: message.Payload}, nil + case uint32(1): + switch keyInfo.Kind { + case Symmetric: + if keyInfo.SymKey == nil { + return nil, errors.New("Symmetric key is required") + } + fmt.Println("AAA") + + decodedData, err := decryptSymmetric(message.Payload, keyInfo.SymKey) + if err != nil { + return nil, errors.New("Couldn't decrypt using symmetric key") + } + + decodedPayload, err := validateAndParse(decodedData) + if err != nil { + return nil, err + } + fmt.Println("AAA") + + return decodedPayload, nil + case Asymmetric: + if keyInfo.PrivKey == nil { + return nil, errors.New("Private key is required") + } + + decodedData, err := decryptAsymmetric(message.Payload, keyInfo.PrivKey) + if err != nil { + return nil, errors.New("Couldn't decrypt using asymmetric key") + } + + decodedPayload, err := validateAndParse(decodedData) + if err != nil { + return nil, err + } + + return decodedPayload, nil + case None: + return nil, errors.New("Non supported KeyKind") + } + } + return nil, errors.New("Unsupported WakuMessage version") +} const aesNonceLength = 12 const aesKeyLength = 32 +const signatureFlag = byte(4) +const flagsLength = 1 +const padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol +const signatureLength = 65 +const sizeMask = byte(3) // Decrypts a message with a topic key, using AES-GCM-256. // nonce size should be 12 bytes (see cipher.gcmStandardNonceSize). @@ -62,49 +171,23 @@ func decryptSymmetric(payload []byte, key []byte) ([]byte, error) { // Decrypts an encrypted payload with a private key. func decryptAsymmetric(payload []byte, key *ecdsa.PrivateKey) ([]byte, error) { decrypted, err := ecies.ImportECDSA(key).Decrypt(payload, nil, nil) - if err == nil { + if err != nil { return nil, err } return decrypted, err } -func DecodePayload(message *protocol.WakuMessage, keyInfo *KeyInfo) ([]byte, error) { - switch *message.Version { - case uint32(0): - return message.Payload, nil - case uint32(1): - switch keyInfo.Kind { - case Symmetric: - decoded, err := decryptSymmetric(message.Payload, keyInfo.SymKey) - if err != nil { - return nil, errors.New("Couldn't decrypt using symmetric key") - } else { - return decoded, nil - } - case Asymmetric: - decoded, err := decryptAsymmetric(message.Payload, &keyInfo.PrivKey) - if err != nil { - return nil, errors.New("Couldn't decrypt using asymmetric key") - } else { - return decoded, nil - } - case None: - return nil, errors.New("Non supported KeyKind") - } - } - return nil, errors.New("Unsupported WakuMessage version") -} - // ValidatePublicKey checks the format of the given public key. -func ValidatePublicKey(k *ecdsa.PublicKey) bool { +func validatePublicKey(k *ecdsa.PublicKey) bool { return k != nil && k.X != nil && k.Y != nil && k.X.Sign() != 0 && k.Y.Sign() != 0 } // Encrypts and returns with a public key. func encryptAsymmetric(rawPayload []byte, key *ecdsa.PublicKey) ([]byte, error) { - if !ValidatePublicKey(key) { + if !validatePublicKey(key) { return nil, errors.New("invalid public key provided for asymmetric encryption") } + encrypted, err := ecies.Encrypt(crand.Reader, ecies.ImportECDSAPublic(key), rawPayload, nil, nil) if err == nil { return encrypted, nil @@ -187,29 +270,154 @@ func generateSecureRandomData(length int) ([]byte, error) { return res, nil } -func Encode(rawPayload []byte, keyInfo *KeyInfo, version uint32) ([]byte, error) { - switch version { - case 0: - return rawPayload, nil - case 1: - switch keyInfo.Kind { - case Symmetric: - encoded, err := encryptSymmetric(rawPayload, keyInfo.SymKey) - if err != nil { - return nil, errors.New("Couldn't encrypt using symmetric key") - } else { - return encoded, nil - } - case Asymmetric: - encoded, err := encryptAsymmetric(rawPayload, &keyInfo.PrivKey.PublicKey) - if err != nil { - return nil, errors.New("Couldn't encrypt using asymmetric key") - } else { - return encoded, nil - } - case None: - return nil, errors.New("Non supported KeyKind") +func isMessageSigned(flags byte) bool { + return (flags & signatureFlag) != 0 +} + +// sign calculates the cryptographic signature for the message, +// also setting the sign flag. +func sign(data []byte, privKey ecdsa.PrivateKey) ([]byte, error) { + result := make([]byte, len(data)) + copy(result, data) + + if isMessageSigned(result[0]) { + // this should not happen, but no reason to panic + return result, nil + } + + result[0] |= signatureFlag // it is important to set this flag before signing + hash := crypto.Keccak256(result) + signature, err := crypto.Sign(hash, &privKey) + + if err != nil { + result[0] &= (0xFF ^ signatureFlag) // clear the flag + return nil, err + } + result = append(result, signature...) + + return result, nil +} + +func (payload Payload) v1Data() ([]byte, error) { + const payloadSizeFieldMaxSize = 4 + result := make([]byte, 1, flagsLength+payloadSizeFieldMaxSize+len(payload.Data)+len(payload.Padding)+signatureLength+padSizeLimit) + result[0] = 0 // set all the flags to zero + result = payload.addPayloadSizeField(result) + result = append(result, payload.Data...) + result, err := payload.appendPadding(result) + return result, err +} + +// addPayloadSizeField appends the auxiliary field containing the size of payload +func (payload Payload) addPayloadSizeField(input []byte) []byte { + fieldSize := getSizeOfPayloadSizeField(payload.Data) + field := make([]byte, 4) + binary.LittleEndian.PutUint32(field, uint32(len(payload.Data))) + field = field[:fieldSize] + result := append(input, field...) + result[0] |= byte(fieldSize) + return result +} + +// getSizeOfPayloadSizeField returns the number of bytes necessary to encode the size of payload +func getSizeOfPayloadSizeField(payload []byte) int { + s := 1 + for i := len(payload); i >= 256; i /= 256 { + s++ + } + return s +} + +// appendPadding appends the padding specified in params. +// If no padding is provided in params, then random padding is generated. +func (payload Payload) appendPadding(input []byte) ([]byte, error) { + if len(payload.Padding) != 0 { + // padding data was provided by the Dapp, just use it as is + result := append(input, payload.Padding...) + return result, nil + } + + rawSize := flagsLength + getSizeOfPayloadSizeField(payload.Data) + len(payload.Data) + if payload.Key.PrivKey != nil { + rawSize += signatureLength + } + odd := rawSize % padSizeLimit + paddingSize := padSizeLimit - odd + pad := make([]byte, paddingSize) + _, err := crand.Read(pad) + if err != nil { + return nil, err + } + if !validateDataIntegrity(pad, paddingSize) { + return nil, errors.New("failed to generate random padding of size " + strconv.Itoa(paddingSize)) + } + result := append(input, pad...) + return result, nil +} + +func validateAndParse(input []byte) (*DecodedPayload, error) { + end := len(input) + if end < 1 { + return nil, errors.New("invalid message length") + } + + msg := new(DecodedPayload) + + if isMessageSigned(input[0]) { + end -= signatureLength + if end <= 1 { + return nil, errors.New("invalid message length") + } + msg.Signature = input[end : end+signatureLength] + + var err error + msg.PubKey, err = msg.sigToPubKey(input) + if err != nil { + return nil, err } } - return nil, errors.New("Unsupported WakuMessage version") + + beg := 1 + payloadSize := 0 + sizeOfPayloadSizeField := int(input[0] & sizeMask) // number of bytes indicating the size of payload + + if sizeOfPayloadSizeField != 0 { + if end < beg+sizeOfPayloadSizeField { + return nil, errors.New("invalid message length") + } + payloadSize = int(bytesToUintLittleEndian(input[beg : beg+sizeOfPayloadSizeField])) + beg += sizeOfPayloadSizeField + if beg+payloadSize > end { + return nil, errors.New("invalid message length") + } + msg.Data = input[beg : beg+payloadSize] + } + + beg += payloadSize + msg.Padding = input[beg:end] + + return msg, nil +} + +// SigToPubKey returns the public key associated to the message's +// signature. +func (p *DecodedPayload) sigToPubKey(input []byte) (*ecdsa.PublicKey, error) { + defer func() { _ = recover() }() // in case of invalid signature + hash := crypto.Keccak256(input[0 : len(input)-signatureLength]) + pub, err := crypto.SigToPub(hash, p.Signature) + if err != nil { + return nil, err + } + + return pub, nil +} + +// bytesToUintLittleEndian converts the slice to 64-bit unsigned integer. +func bytesToUintLittleEndian(b []byte) (res uint64) { + mul := uint64(1) + for i := 0; i < len(b); i++ { + res += uint64(b[i]) * mul + mul *= 256 + } + return res }