2022-04-04 15:46:51 +00:00
# Waku Noise Protocols for Waku Payload Encryption
2022-08-04 08:47:00 +00:00
# Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
2022-04-04 15:46:51 +00:00
## 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
2024-06-28 10:34:57 +00:00
{. push raises : [ ] . }
2022-04-04 15:46:51 +00:00
2022-11-04 09:52:08 +00:00
import std / [ options , strutils ]
import stew / byteutils
2022-04-04 15:46:51 +00:00
import chronos
import chronicles
2022-09-07 15:31:27 +00:00
import bearssl / rand
2022-11-04 09:52:08 +00:00
import stew / endians2
import nimcrypto / [ sha2 , hmac ]
2022-04-04 15:46:51 +00:00
import libp2p / utility
2022-08-04 08:47:00 +00:00
import libp2p / crypto / [ crypto , chacha20poly1305 , hkdf ]
2022-05-20 13:51:36 +00:00
import libp2p / protocols / secure / secure
2022-04-06 12:37:02 +00:00
2022-08-04 08:47:00 +00:00
import . / noise_types
2022-04-04 15:46:51 +00:00
logScope :
2022-11-03 15:36:24 +00:00
topics = " waku noise "
2022-04-04 15:46:51 +00:00
2022-04-06 12:37:02 +00:00
#################################################################
2022-05-20 13:51:36 +00:00
# Noise state machine primitives
# Overview :
# - Alice and Bob process (i.e. read and write, based on their role) each token appearing in a handshake pattern, consisting of pre-message and message patterns;
# - Both users initialize and update according to processed tokens a Handshake State, a Symmetric State and a Cipher State;
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
# - When a static public key s or a payload message is writted, it is encrypted with encryptAndHash;
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
# - If all tokens are processed, users compute two new Cipher States by calling Split;
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
#################################
# Cipher State Primitives
#################################
# Checks if a Cipher State has an encryption key set
2022-08-04 08:47:00 +00:00
proc hasKey * ( cs : CipherState ) : bool =
2022-05-20 13:51:36 +00:00
return ( cs . k ! = EmptyKey )
# Encrypts a plaintext using key material in a Noise Cipher State
# The CipherState is updated increasing the nonce (used as a counter in Noise) by one
2024-03-15 23:08:47 +00:00
proc encryptWithAd * (
state : var CipherState , ad , plaintext : openArray [ byte ]
) : seq [ byte ] {. raises : [ Defect , NoiseNonceMaxError ] . } =
2022-05-20 13:51:36 +00:00
# We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
if state . n > NonceMax :
raise newException ( NoiseNonceMaxError , " Noise max nonce value reached " )
var ciphertext : seq [ byte ]
# If an encryption key is set in the Cipher state, we proceed with encryption
if state . hasKey :
# The output is the concatenation of the ciphertext and authorization tag
# We define its length accordingly
ciphertext = newSeqOfCap [ byte ] ( plaintext . len + sizeof ( ChaChaPolyTag ) )
2024-03-15 23:08:47 +00:00
2022-05-20 13:51:36 +00:00
# Since ChaChaPoly encryption primitive overwrites the input with the output,
# we copy the plaintext in the output ciphertext variable and we pass it to encryption
ciphertext . add ( plaintext )
# The nonce is read from the input CipherState
# By Noise specification the nonce is 8 bytes long out of the 12 bytes supported by ChaChaPoly
var nonce : ChaChaPolyNonce
2024-03-15 23:08:47 +00:00
nonce [ 4 .. < 12 ] = toBytesLE ( state . n )
2022-05-20 13:51:36 +00:00
# We perform encryption and we store the authorization tag
var authorizationTag : ChaChaPolyTag
ChaChaPoly . encrypt ( state . k , nonce , authorizationTag , ciphertext , ad )
# We append the authorization tag to ciphertext
ciphertext . add ( authorizationTag )
# We increase the Cipher state nonce
inc state . n
# If the nonce is greater than the maximum allowed nonce, we raise an exception
if state . n > NonceMax :
raise newException ( NoiseNonceMaxError , " Noise max nonce value reached " )
2024-03-15 23:08:47 +00:00
trace " encryptWithAd " ,
authorizationTag = byteutils . toHex ( authorizationTag ) ,
ciphertext = ciphertext ,
nonce = state . n - 1
2022-05-20 13:51:36 +00:00
# Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
else :
ciphertext = @ plaintext
debug " encryptWithAd called with no encryption key set. Returning plaintext. "
return ciphertext
# Decrypts a ciphertext using key material in a Noise Cipher State
# The CipherState is updated increasing the nonce (used as a counter in Noise) by one
2024-03-15 23:08:47 +00:00
proc decryptWithAd * (
state : var CipherState , ad , ciphertext : openArray [ byte ]
) : seq [ byte ] {. raises : [ Defect , NoiseDecryptTagError , NoiseNonceMaxError ] . } =
2022-05-20 13:51:36 +00:00
# We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
if state . n > NonceMax :
raise newException ( NoiseNonceMaxError , " Noise max nonce value reached " )
var plaintext : seq [ byte ]
# If an encryption key is set in the Cipher state, we proceed with decryption
if state . hasKey :
# We read the authorization appendend at the end of a ciphertext
2024-03-15 23:08:47 +00:00
let inputAuthorizationTag = ciphertext . toOpenArray (
ciphertext . len - ChaChaPolyTag . len , ciphertext . high
) . intoChaChaPolyTag
2022-05-20 13:51:36 +00:00
var
authorizationTag : ChaChaPolyTag
nonce : ChaChaPolyNonce
# The nonce is read from the input CipherState
# By Noise specification the nonce is 8 bytes long out of the 12 bytes supported by ChaChaPoly
2024-03-15 23:08:47 +00:00
nonce [ 4 .. < 12 ] = toBytesLE ( state . n )
2022-05-20 13:51:36 +00:00
# Since ChaChaPoly decryption primitive overwrites the input with the output,
# we copy the ciphertext (authorization tag excluded) in the output plaintext variable and we pass it to decryption
2024-03-15 23:08:47 +00:00
plaintext = ciphertext [ 0 .. ( ciphertext . high - ChaChaPolyTag . len ) ]
2022-05-20 13:51:36 +00:00
ChaChaPoly . decrypt ( state . k , nonce , authorizationTag , plaintext , ad )
# We check if the input authorization tag matches the decryption authorization tag
if inputAuthorizationTag ! = authorizationTag :
2024-03-15 23:08:47 +00:00
debug " decryptWithAd failed " ,
plaintext = plaintext ,
ciphertext = ciphertext ,
inputAuthorizationTag = inputAuthorizationTag ,
authorizationTag = authorizationTag
raise
newException ( NoiseDecryptTagError , " decryptWithAd failed tag authentication. " )
2022-05-20 13:51:36 +00:00
# We increase the Cipher state nonce
inc state . n
# If the nonce is greater than the maximum allowed nonce, we raise an exception
if state . n > NonceMax :
raise newException ( NoiseNonceMaxError , " Noise max nonce value reached " )
2024-03-15 23:08:47 +00:00
trace " decryptWithAd " ,
inputAuthorizationTag = inputAuthorizationTag ,
authorizationTag = authorizationTag ,
nonce = state . n
2022-05-20 13:51:36 +00:00
# Otherwise we return the input ciphertext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
else :
plaintext = @ ciphertext
debug " decryptWithAd called with no encryption key set. Returning ciphertext. "
return plaintext
# Sets the nonce of a Cipher State
proc setNonce * ( cs : var CipherState , nonce : uint64 ) =
cs . n = nonce
# Sets the key of a Cipher State
proc setCipherStateKey * ( cs : var CipherState , key : ChaChaPolyKey ) =
cs . k = key
# Generates a random Symmetric Cipher State for test purposes
2022-09-07 15:31:27 +00:00
proc randomCipherState * ( rng : var HmacDrbgContext , nonce : uint64 = 0 ) : CipherState =
2022-05-20 13:51:36 +00:00
var randomCipherState : CipherState
2022-09-07 15:31:27 +00:00
hmacDrbgGenerate ( rng , randomCipherState . k )
2022-05-20 13:51:36 +00:00
setNonce ( randomCipherState , nonce )
return randomCipherState
# Gets the key of a Cipher State
proc getKey * ( cs : CipherState ) : ChaChaPolyKey =
return cs . k
# Gets the nonce of a Cipher State
proc getNonce * ( cs : CipherState ) : uint64 =
return cs . n
#################################
# Symmetric State primitives
#################################
# Initializes a Symmetric State
proc init * ( _ : type [ SymmetricState ] , hsPattern : HandshakePattern ) : SymmetricState =
var ss : SymmetricState
# We compute the hash of the protocol name
ss . h = hsPattern . name . hashProtocol
# We initialize the chaining key ck
ss . ck = ss . h . data . intoChaChaPolyKey
# We initialize the Cipher state
ss . cs = CipherState ( k : EmptyKey )
return ss
# MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Updates a Symmetric state chaining key and symmetric state
2022-06-03 19:12:41 +00:00
proc mixKey * ( ss : var SymmetricState , inputKeyMaterial : openArray [ byte ] ) =
2022-05-20 13:51:36 +00:00
# We derive two keys using HKDF
var tempKeys : array [ 2 , ChaChaPolyKey ]
sha256 . hkdf ( ss . ck , inputKeyMaterial , [ ] , tempKeys )
# We update ck and the Cipher state's key k using the output of HDKF
ss . ck = tempKeys [ 0 ]
ss . cs = CipherState ( k : tempKeys [ 1 ] )
trace " mixKey " , ck = ss . ck , k = ss . cs . k
# MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Hashes data into a Symmetric State's handshake hash value h
proc mixHash * ( ss : var SymmetricState , data : openArray [ byte ] ) =
# We prepare the hash context
var ctx : sha256
ctx . init ( )
# We add the previous handshake hash
ctx . update ( ss . h . data )
# We append the input data
ctx . update ( data )
# We hash and store the result in the Symmetric State's handshake hash value
ss . h = ctx . finish ( )
trace " mixHash " , hash = ss . h . data
# mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Combines MixKey and MixHash
2024-03-15 23:08:47 +00:00
proc mixKeyAndHash * (
ss : var SymmetricState , inputKeyMaterial : openArray [ byte ]
) {. used . } =
2022-05-20 13:51:36 +00:00
var tempKeys : array [ 3 , ChaChaPolyKey ]
# Derives 3 keys using HKDF, the chaining key and the input key material
sha256 . hkdf ( ss . ck , inputKeyMaterial , [ ] , tempKeys )
# Sets the chaining key
ss . ck = tempKeys [ 0 ]
# Updates the handshake hash value
ss . mixHash ( tempKeys [ 1 ] )
# Updates the Cipher state's key
# Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes."
ss . cs = CipherState ( k : tempKeys [ 2 ] )
# EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Combines encryptWithAd and mixHash
2022-09-12 00:23:14 +00:00
# Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
2024-03-15 23:08:47 +00:00
proc encryptAndHash * (
ss : var SymmetricState , plaintext : openArray [ byte ] , extraAd : openArray [ byte ] = [ ]
) : seq [ byte ] {. raises : [ Defect , NoiseNonceMaxError ] . } =
2022-05-20 13:51:36 +00:00
# The output ciphertext
var ciphertext : seq [ byte ]
2022-09-12 00:23:14 +00:00
# The additional data
let ad = @ ( ss . h . data ) & @ ( extraAd )
2022-05-20 13:51:36 +00:00
# Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintex
2022-09-12 00:23:14 +00:00
ciphertext = ss . cs . encryptWithAd ( ad , plaintext )
2022-05-20 13:51:36 +00:00
# We call mixHash over the result
ss . mixHash ( ciphertext )
return ciphertext
# DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Combines decryptWithAd and mixHash
2024-03-15 23:08:47 +00:00
proc decryptAndHash * (
ss : var SymmetricState , ciphertext : openArray [ byte ] , extraAd : openArray [ byte ] = [ ]
) : seq [ byte ] {. raises : [ Defect , NoiseDecryptTagError , NoiseNonceMaxError ] . } =
2022-05-20 13:51:36 +00:00
# The output plaintext
var plaintext : seq [ byte ]
2022-09-12 00:23:14 +00:00
# The additional data
let ad = @ ( ss . h . data ) & @ ( extraAd )
2022-05-20 13:51:36 +00:00
# Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
2022-09-12 00:23:14 +00:00
plaintext = ss . cs . decryptWithAd ( ad , ciphertext )
2022-05-20 13:51:36 +00:00
# According to specification, the ciphertext enters mixHash (and not the plaintext)
ss . mixHash ( ciphertext )
return plaintext
# Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
proc split * ( ss : var SymmetricState ) : tuple [ cs1 , cs2 : CipherState ] =
# Derives 2 keys using HKDF and the chaining key
var tempKeys : array [ 2 , ChaChaPolyKey ]
sha256 . hkdf ( ss . ck , [ ] , [ ] , tempKeys )
# Returns a tuple of two Cipher States initialized with the derived keys
return ( CipherState ( k : tempKeys [ 0 ] ) , CipherState ( k : tempKeys [ 1 ] ) )
# Gets the chaining key field of a Symmetric State
proc getChainingKey * ( ss : SymmetricState ) : ChaChaPolyKey =
return ss . ck
# Gets the handshake hash field of a Symmetric State
proc getHandshakeHash * ( ss : SymmetricState ) : MDigest [ 256 ] =
return ss . h
# Gets the Cipher State field of a Symmetric State
proc getCipherState * ( ss : SymmetricState ) : CipherState =
return ss . cs
#################################
# Handshake State primitives
#################################
# Initializes a Handshake State
2024-03-15 23:08:47 +00:00
proc init * (
_ : type [ HandshakeState ] , hsPattern : HandshakePattern , psk : seq [ byte ] = @ [ ]
) : HandshakeState =
2022-05-20 13:51:36 +00:00
# The output Handshake State
var hs : HandshakeState
# By default the Handshake State initiator flag is set to false
# Will be set to true when the user associated to the handshake state starts an handshake
hs . initiator = false
# We copy the information on the handshake pattern for which the state is initialized (protocol name, handshake pattern, psk)
hs . handshakePattern = hsPattern
hs . psk = psk
# We initialize the Symmetric State
hs . ss = SymmetricState . init ( hsPattern )
return hs
2022-04-06 12:37:02 +00:00
2022-04-04 15:46:51 +00:00
#################################################################
2022-05-20 13:51:36 +00:00
#################################
2022-04-04 15:46:51 +00:00
# ChaChaPoly Symmetric Cipher
2022-05-20 13:51:36 +00:00
#################################
2022-04-04 15:46:51 +00:00
# ChaChaPoly encryption
# It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext
2022-04-06 12:37:02 +00:00
# The cipher state in not changed
2022-04-04 15:46:51 +00:00
proc encrypt * (
2024-03-15 23:08:47 +00:00
state : ChaChaPolyCipherState , plaintext : openArray [ byte ]
) : ChaChaPolyCiphertext {. noinit , raises : [ Defect , NoiseEmptyChaChaPolyInput ] . } =
2022-04-06 12:37:02 +00:00
# If plaintext is empty, we raise an error
if plaintext = = @ [ ] :
raise newException ( NoiseEmptyChaChaPolyInput , " Tried to encrypt empty plaintext " )
2022-04-04 15:46:51 +00:00
var ciphertext : ChaChaPolyCiphertext
2022-04-06 12:37:02 +00:00
# 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
2022-04-04 15:46:51 +00:00
ciphertext . data . add plaintext
2022-06-03 19:12:41 +00:00
# TODO: add padding
2022-04-06 12:37:02 +00:00
# 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)
2022-04-04 15:46:51 +00:00
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
2022-04-06 12:37:02 +00:00
# The cipher state is not changed
2022-04-04 15:46:51 +00:00
proc decrypt * (
2024-03-15 23:08:47 +00:00
state : ChaChaPolyCipherState , ciphertext : ChaChaPolyCiphertext
) : seq [ byte ] {. raises : [ Defect , NoiseEmptyChaChaPolyInput , NoiseDecryptTagError ] . } =
2022-04-06 12:37:02 +00:00
# If ciphertext is empty, we raise an error
if ciphertext . data = = @ [ ] :
raise newException ( NoiseEmptyChaChaPolyInput , " Tried to decrypt empty ciphertext " )
2022-04-04 15:46:51 +00:00
var
2022-04-06 12:37:02 +00:00
# The input authorization tag
2022-04-04 15:46:51 +00:00
tagIn = ciphertext . tag
2022-04-06 12:37:02 +00:00
# The authorization tag computed during decryption
2022-04-04 15:46:51 +00:00
tagOut : ChaChaPolyTag
2022-04-06 12:37:02 +00:00
# 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
2022-04-04 15:46:51 +00:00
var plaintext = ciphertext . data
2022-04-06 12:37:02 +00:00
# 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)
2022-04-04 15:46:51 +00:00
ChaChaPoly . decrypt ( state . k , state . nonce , tagOut , plaintext , state . ad )
2022-06-03 19:12:41 +00:00
# TODO: add unpadding
2022-05-20 13:51:36 +00:00
trace " decrypt " , tagIn = tagIn , tagOut = tagOut , nonce = state . nonce
2022-04-06 12:37:02 +00:00
# We check if the authorization tag computed while decrypting is the same as the input tag
2022-04-04 15:46:51 +00:00
if tagIn ! = tagOut :
debug " decrypt failed " , plaintext = shortLog ( plaintext )
raise newException ( NoiseDecryptTagError , " decrypt tag authentication failed. " )
2024-03-15 23:08:47 +00:00
return plaintext