# Waku Noise Protocols for Waku Payload Encryption ## See spec for more details: ## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35 ## ## Implementation partially inspired by noise-libp2p: ## https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim {.push raises: [Defect].} import std/[oids, options, tables] import chronos import chronicles import bearssl import strutils import stew/[endians2] import nimcrypto/[utils, sha2, hmac] import libp2p/stream/[connection] import libp2p/peerid import libp2p/peerinfo import libp2p/protobuf/minprotobuf import libp2p/utility import libp2p/errors import libp2p/crypto/[crypto, chacha20poly1305, curve25519] logScope: topics = "wakunoise" ################################################################# # Constants and data structures const # EmptyKey represents a non-initialized ChaChaPolyKey EmptyKey = default(ChaChaPolyKey) # The maximum ChaChaPoly allowed nonce in Noise Handshakes NonceMax = uint64.high - 1 type # Default underlying elliptic curve arithmetic (useful for switching to multiple ECs) # Current default is Curve25519 EllipticCurveKey = Curve25519Key # An EllipticCurveKey (public, private) key pair KeyPair* = object privateKey: EllipticCurveKey publicKey: EllipticCurveKey # A Noise public key is a public key exchanged during Noise handshakes (no private part) # This follows https://rfc.vac.dev/spec/35/#public-keys-serialization # pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0) # or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1) NoisePublicKey* = object flag: uint8 pk: seq[byte] # A ChaChaPoly ciphertext (data) + authorization tag (tag) ChaChaPolyCiphertext* = object data*: seq[byte] tag*: ChaChaPolyTag # A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad) ChaChaPolyCipherState* = object k: ChaChaPolyKey nonce: ChaChaPolyNonce ad: seq[byte] # Some useful error types NoiseError* = object of LPError NoiseHandshakeError* = object of NoiseError NoiseEmptyChaChaPolyInput* = object of NoiseError NoiseDecryptTagError* = object of NoiseError NoiseNonceMaxError* = object of NoiseError NoisePublicKeyError* = object of NoiseError NoiseMalformedHandshake* = object of NoiseError ################################################################# # Utilities # Generates random byte sequences of given size proc randomSeqByte*(rng: var BrHmacDrbgContext, size: int): seq[byte] = var output = newSeq[byte](size.uint32) brHmacDrbgGenerate(rng, output) return output # Generate random Curve25519 (public, private) key pairs proc genKeyPair*(rng: var BrHmacDrbgContext): KeyPair = var keyPair: KeyPair keyPair.privateKey = EllipticCurveKey.random(rng) keyPair.publicKey = keyPair.privateKey.public() return keyPair ################################################################# # ChaChaPoly Symmetric Cipher # ChaChaPoly encryption # It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext # The cipher state in not changed proc encrypt*( state: ChaChaPolyCipherState, plaintext: openArray[byte]): ChaChaPolyCiphertext {.noinit, raises: [Defect, NoiseEmptyChaChaPolyInput].} = # If plaintext is empty, we raise an error if plaintext == @[]: raise newException(NoiseEmptyChaChaPolyInput, "Tried to encrypt empty plaintext") var ciphertext: ChaChaPolyCiphertext # Since ChaChaPoly's library "encrypt" primitive directly changes the input plaintext to the ciphertext, # we copy the plaintext into the ciphertext variable and we pass the latter to encrypt ciphertext.data.add plaintext #TODO: add padding # ChaChaPoly.encrypt takes as input: the key (k), the nonce (nonce), a data structure for storing the computed authorization tag (tag), # the plaintext (overwritten to ciphertext) (data), the associated data (ad) ChaChaPoly.encrypt(state.k, state.nonce, ciphertext.tag, ciphertext.data, state.ad) return ciphertext # ChaChaPoly decryption # It takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext # The cipher state is not changed proc decrypt*( state: ChaChaPolyCipherState, ciphertext: ChaChaPolyCiphertext): seq[byte] {.raises: [Defect, NoiseEmptyChaChaPolyInput, NoiseDecryptTagError].} = # If ciphertext is empty, we raise an error if ciphertext.data == @[]: raise newException(NoiseEmptyChaChaPolyInput, "Tried to decrypt empty ciphertext") var # The input authorization tag tagIn = ciphertext.tag # The authorization tag computed during decryption tagOut: ChaChaPolyTag # Since ChaChaPoly's library "decrypt" primitive directly changes the input ciphertext to the plaintext, # we copy the ciphertext into the plaintext variable and we pass the latter to decrypt var plaintext = ciphertext.data # ChaChaPoly.decrypt takes as input: the key (k), the nonce (nonce), a data structure for storing the computed authorization tag (tag), # the ciphertext (overwritten to plaintext) (data), the associated data (ad) ChaChaPoly.decrypt(state.k, state.nonce, tagOut, plaintext, state.ad) #TODO: add unpadding trace "decrypt", tagIn = tagIn.shortLog, tagOut = tagOut.shortLog, nonce = state.nonce # We check if the authorization tag computed while decrypting is the same as the input tag if tagIn != tagOut: debug "decrypt failed", plaintext = shortLog(plaintext) raise newException(NoiseDecryptTagError, "decrypt tag authentication failed.") return plaintext # Generates a random ChaChaPoly Cipher State for testing encryption/decryption proc randomChaChaPolyCipherState*(rng: var BrHmacDrbgContext): ChaChaPolyCipherState = var randomCipherState: ChaChaPolyCipherState brHmacDrbgGenerate(rng, randomCipherState.k) brHmacDrbgGenerate(rng, randomCipherState.nonce) randomCipherState.ad = newSeq[byte](32) brHmacDrbgGenerate(rng, randomCipherState.ad) return randomCipherState ################################################################# # Noise Public keys # 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, private) Elliptic Curve keypair to an unencrypted Noise public key (only public part) proc keyPairToNoisePublicKey*(keyPair: KeyPair): NoisePublicKey = var noisePublicKey: NoisePublicKey noisePublicKey.flag = 0 noisePublicKey.pk = getBytes(keyPair.publicKey) return noisePublicKey # Generates a random Noise public key proc genNoisePublicKey*(rng: var BrHmacDrbgContext): 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.. 256: debug "Payload malformed: too many public keys contained in the handshake message" return none(seq[byte]) let transport_message_len = self.transport_message.len #let transport_message_len_len = ceil(log(transport_message_len, 8)).int var payload = newSeqOfCap[byte](1 + #self.protocol_id.len + 1 + #ser_handshake_message_len ser_handshake_message_len + 8 + #transport_message_len transport_message_len #self.transport_message ) payload.add self.protocol_id.byte payload.add ser_handshake_message_len.byte payload.add ser_handshake_message payload.add toBytesLE(transport_message_len.uint64) payload.add self.transport_message return some(payload) #Decode Noise handshake payload proc decodeV2*(payload: seq[byte]): Option[PayloadV2] = var res: PayloadV2 var i: uint64 = 0 res.protocol_id = payload[i].uint8 i+=1 var handshake_message_len = payload[i].uint64 i+=1 var handshake_message: seq[NoisePublicKey] var flag: byte pk_len: uint64 written: uint64 = 0 while written != handshake_message_len: #Note that flag can be used to add support to multiple Elliptic Curve arithmetics.. flag = payload[i] if flag == 0: pk_len = 1 + Curve25519Key.len handshake_message.add intoNoisePublicKey(payload[i..