# Waku Noise Protocols for Waku Payload Encryption # Noise utilities module ## See spec for more details: ## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35 when (NimMajor, NimMinor) < (1, 4): {.push raises: [Defect].} else: {.push raises: [].} import std/[algorithm, base64, oids, options, strutils, tables, sequtils] import chronos import chronicles import bearssl/rand import stew/[results, endians2, byteutils] import nimcrypto/[sha2, hmac] import libp2p/crypto/[chacha20poly1305, curve25519, hkdf] import ./noise_types import ./noise logScope: topics = "waku noise" ################################################################# ################################# # Generic Utilities ################################# # Generates random byte sequences of given size proc randomSeqByte*(rng: var HmacDrbgContext, size: int): seq[byte] = var output = newSeq[byte](size.uint32) hmacDrbgGenerate(rng, output) return output # Pads a payload according to PKCS#7 as per RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3 proc pkcs7_pad*(payload: seq[byte], paddingSize: int): seq[byte] = assert(paddingSize<256) let k = paddingSize - (payload.len mod paddingSize) var padding: seq[byte] if k != 0: padding = newSeqWith(k, k.byte) else: padding = newSeqWith(paddingSize, paddingSize.byte) let padded = concat(payload, padding) return padded # Unpads a payload according to PKCS#7 as per RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3 proc pkcs7_unpad*(payload: seq[byte], paddingSize: int): seq[byte] = let k = payload[payload.high] let unpadded = payload[0..payload.high-k.int] return unpadded proc seqToDigest256*(sequence: seq[byte]): MDigest[256] = var digest: MDigest[256] for i in 0.. 0: raise newException(NoiseSomeMessagesWereLost, "Message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost") # index is 0, hence the read message tag is the next expected one return ok(true) # Deletes the first n elements in buffer and appends n new ones proc pop*(mntb: var MessageNametagBuffer): MessageNametag = # Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned let messageNametag = mntb.buffer[0] delete(mntb, 1) return messageNametag # Performs a Diffie-Hellman operation between two elliptic curve keys (one private, one public) proc dh*(private: EllipticCurveKey, public: EllipticCurveKey): EllipticCurveKey = # The output result of the Diffie-Hellman operation var output: EllipticCurveKey # Since the EC multiplication writes the result to the input, we copy the input to the output variable output = public # We execute the DH operation EllipticCurve.mul(output, private) return output ################################################################# ################################# # ChaChaPoly Cipher utilities ################################# # Generates a random ChaChaPolyKey for testing encryption/decryption proc randomChaChaPolyKey*(rng: var HmacDrbgContext): ChaChaPolyKey = var key: ChaChaPolyKey hmacDrbgGenerate(rng, key) return key # Generates a random ChaChaPoly Cipher State for testing encryption/decryption proc randomChaChaPolyCipherState*(rng: var HmacDrbgContext): ChaChaPolyCipherState = var randomCipherState: ChaChaPolyCipherState randomCipherState.k = randomChaChaPolyKey(rng) hmacDrbgGenerate(rng, randomCipherState.nonce) randomCipherState.ad = newSeq[byte](32) hmacDrbgGenerate(rng, randomCipherState.ad) return randomCipherState ################################################################# ################################# # Noise Public keys utilities ################################# # Checks equality between two Noise public keys proc `==`*(k1, k2: NoisePublicKey): bool = return (k1.flag == k2.flag) and (k1.pk == k2.pk) # Converts a public Elliptic Curve key to an unencrypted Noise public key proc toNoisePublicKey*(publicKey: EllipticCurveKey): NoisePublicKey = var noisePublicKey: NoisePublicKey noisePublicKey.flag = 0 noisePublicKey.pk = getBytes(publicKey) return noisePublicKey # Generates a random Noise public key proc genNoisePublicKey*(rng: var HmacDrbgContext): NoisePublicKey = var noisePublicKey: NoisePublicKey # We generate a random key pair let keyPair: KeyPair = genKeyPair(rng) # Since it is unencrypted, flag is 0 noisePublicKey.flag = 0 # We copy the public X coordinate of the key pair to the output Noise public key noisePublicKey.pk = getBytes(keyPair.publicKey) return noisePublicKey # Converts a Noise public key to a stream of bytes as in # https://rfc.vac.dev/spec/35/#public-keys-serialization proc serializeNoisePublicKey*(noisePublicKey: NoisePublicKey): seq[byte] = var serializedNoisePublicKey: seq[byte] # Public key is serialized as (flag || pk) # Note that pk contains the X coordinate of the public key if unencrypted # or the encryption concatenated with the authorization tag if encrypted serializedNoisePublicKey.add noisePublicKey.flag serializedNoisePublicKey.add noisePublicKey.pk return serializedNoisePublicKey # Converts a serialized Noise public key to a NoisePublicKey object as in # https://rfc.vac.dev/spec/35/#public-keys-serialization proc intoNoisePublicKey*(serializedNoisePublicKey: seq[byte]): NoisePublicKey {.raises: [Defect, NoisePublicKeyError].} = var noisePublicKey: NoisePublicKey # We retrieve the encryption flag noisePublicKey.flag = serializedNoisePublicKey[0] # If not 0 or 1 we raise a new exception if not (noisePublicKey.flag == 0 or noisePublicKey.flag == 1): raise newException(NoisePublicKeyError, "Invalid flag in serialized public key") # We set the remaining sequence to the pk value (this may be an encrypted or not encrypted X coordinate) noisePublicKey.pk = serializedNoisePublicKey[1.. uint8.high.int: debug "PayloadV2 malformed: too many public keys contained in the handshake message" return err("Too many public keys in handshake message") # We get the transport message byte length let transportMessageLen = self.transportMessage.len # The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as # payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) # We declare it as a byte sequence of length accordingly to the PayloadV2 information read var payload = newSeqOfCap[byte](MessageNametagLength + #MessageNametagLength bytes for messageNametag 1 + # 1 byte for protocol ID 1 + # 1 byte for length of serializedHandshakeMessage field serializedHandshakeMessageLen + # serializedHandshakeMessageLen bytes for serializedHandshakeMessage 8 + # 8 bytes for transportMessageLen transportMessageLen # transportMessageLen bytes for transportMessage ) # We concatenate all the data # The protocol ID (1 byte) and handshake message length (1 byte) can be directly casted to byte to allow direct copy to the payload byte sequence payload.add @(self.messageNametag) payload.add self.protocolId.byte payload.add serializedHandshakeMessageLen.byte payload.add serializedHandshakeMessage # The transport message length is converted from uint64 to bytes in Little-Endian payload.add toBytesLE(transportMessageLen.uint64) payload.add self.transportMessage return ok(payload) # Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/. # The input serialized payload concatenates the output PayloadV2 object fields as # payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) proc deserializePayloadV2*(payload: seq[byte]): Result[PayloadV2, cstring] {.raises: [Defect, NoisePublicKeyError].} = # The output PayloadV2 var payload2: PayloadV2 # i is the read input buffer position index var i: uint64 = 0 # We start by reading the messageNametag for j in 0.. uint8.high.uint64: debug "Payload malformed: too many public keys contained in the handshake message" return err("Too many public keys in handshake message") i += 1 # We now read for handshakeMessageLen bytes the buffer and we deserialize each (encrypted/unencrypted) public key read var # In handshakeMessage we accumulate the read deserialized Noise Public keys handshakeMessage: seq[NoisePublicKey] flag: byte pkLen: uint64 written: uint64 = 0 # We read the buffer until handshakeMessageLen are read while written != handshakeMessageLen: # We obtain the current Noise Public key encryption flag flag = payload[i] # If the key is unencrypted, we only read the X coordinate of the EC public key and we deserialize into a Noise Public Key if flag == 0: pkLen = 1 + EllipticCurveKey.len handshakeMessage.add intoNoisePublicKey(payload[i..