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
2022-11-04 09:52:27 +00:00
when ( NimMajor , NimMinor ) < ( 1 , 4 ) :
{. push raises : [ Defect ] . }
else :
{. 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
proc encryptWithAd * ( state : var CipherState , ad , plaintext : openArray [ byte ] ) : seq [ byte ]
{. raises : [ Defect , NoiseNonceMaxError ] . } =
# 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 ) )
# 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
nonce [ 4 .. < 12 ] = toBytesLE ( state . n )
# 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 " )
trace " encryptWithAd " , authorizationTag = byteutils . toHex ( authorizationTag ) , ciphertext = ciphertext , nonce = state . n - 1
# 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
proc decryptWithAd * ( state : var CipherState , ad , ciphertext : openArray [ byte ] ) : seq [ byte ]
{. raises : [ Defect , NoiseDecryptTagError , NoiseNonceMaxError ] . } =
# 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
let inputAuthorizationTag = ciphertext . toOpenArray ( ciphertext . len - ChaChaPolyTag . len , ciphertext . high ) . intoChaChaPolyTag
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
nonce [ 4 .. < 12 ] = toBytesLE ( state . n )
# 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
plaintext = ciphertext [ 0 .. ( ciphertext . high - ChaChaPolyTag . len ) ]
ChaChaPoly . decrypt ( state . k , nonce , authorizationTag , plaintext , ad )
# We check if the input authorization tag matches the decryption authorization tag
if inputAuthorizationTag ! = authorizationTag :
debug " decryptWithAd failed " , plaintext = plaintext , ciphertext = ciphertext , inputAuthorizationTag = inputAuthorizationTag , authorizationTag = authorizationTag
raise newException ( NoiseDecryptTagError , " decryptWithAd failed tag authentication. " )
# 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 " )
trace " decryptWithAd " , inputAuthorizationTag = inputAuthorizationTag , authorizationTag = authorizationTag , nonce = state . n
# 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
proc mixKeyAndHash * ( ss : var SymmetricState , inputKeyMaterial : openArray [ byte ] ) {. used . } =
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)
proc encryptAndHash * ( ss : var SymmetricState , plaintext : openArray [ byte ] , extraAd : openArray [ byte ] = [ ] ) : seq [ byte ]
2022-05-20 13:51:36 +00:00
{. raises : [ Defect , NoiseNonceMaxError ] . } =
# 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
2022-09-12 00:23:14 +00:00
proc decryptAndHash * ( ss : var SymmetricState , ciphertext : openArray [ byte ] , extraAd : openArray [ byte ] = [ ] ) : seq [ byte ]
2022-05-20 13:51:36 +00:00
{. raises : [ Defect , NoiseDecryptTagError , NoiseNonceMaxError ] . } =
# 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
proc init * ( _ : type [ HandshakeState ] , hsPattern : HandshakePattern , psk : seq [ byte ] = @ [ ] ) : HandshakeState =
# 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 * (
state : ChaChaPolyCipherState ,
plaintext : openArray [ byte ] ) : ChaChaPolyCiphertext
2022-04-06 12:37:02 +00:00
{. noinit , raises : [ Defect , NoiseEmptyChaChaPolyInput ] . } =
# 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 * (
state : ChaChaPolyCipherState ,
ciphertext : ChaChaPolyCiphertext ) : seq [ byte ]
2022-04-06 12:37:02 +00:00
{. raises : [ Defect , NoiseEmptyChaChaPolyInput , NoiseDecryptTagError ] . } =
# 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. " )
2022-08-04 08:47:00 +00:00
return plaintext