2022-12-16 16:05:35 -04:00
import debug from "debug" ;
2022-11-13 09:39:26 -04:00
import { fromString as uint8ArrayFromString } from "uint8arrays" ;
import { concat as uint8ArrayConcat } from "uint8arrays/concat" ;
import { equals as uint8ArrayEquals } from "uint8arrays/equals" ;
import type { bytes32 } from "./@types/basic.js" ;
2022-11-15 17:56:25 -04:00
import { chaCha20Poly1305Decrypt , chaCha20Poly1305Encrypt , getHKDF , hashSHA256 } from "./crypto.js" ;
2022-11-13 09:39:26 -04:00
import { Nonce } from "./nonce.js" ;
import { HandshakePattern } from "./patterns.js" ;
2022-12-16 16:05:35 -04:00
const log = debug ( "waku:noise:handshake-state" ) ;
2022-11-13 09:39:26 -04:00
// Waku Noise Protocols for Waku Payload Encryption
// Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
// See spec for more details:
// https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
//
// Implementation partially inspired by noise-libp2p and js-libp2p-noise
// https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
// https://github.com/ChainSafe/js-libp2p-noise
/ *
# 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 ;
2022-11-14 14:46:42 -04:00
# - When a static public key s or a payload message is written , it is encrypted with encryptAndHash ;
2022-11-13 09:39:26 -04:00
# - 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
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
* /
2022-11-16 11:22:56 -04:00
export function createEmptyKey ( ) : bytes32 {
return new Uint8Array ( 32 ) ;
}
export function isEmptyKey ( k : bytes32 ) : boolean {
const emptyKey = createEmptyKey ( ) ;
return uint8ArrayEquals ( emptyKey , k ) ;
}
2022-11-13 09:39:26 -04:00
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
// Contains an encryption key k and a nonce n (used in Noise as a counter)
export class CipherState {
k : bytes32 ;
// For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n : Nonce ;
2022-11-16 11:22:56 -04:00
constructor ( k : bytes32 = createEmptyKey ( ) , n = new Nonce ( ) ) {
2022-11-13 09:39:26 -04:00
this . k = k ;
2022-11-15 17:56:25 -04:00
this . n = n ;
}
clone ( ) : CipherState {
return new CipherState ( new Uint8Array ( this . k ) , new Nonce ( this . n . getUint64 ( ) ) ) ;
}
equals ( b : CipherState ) : boolean {
return uint8ArrayEquals ( this . k , b . getKey ( ) ) && this . n . getUint64 ( ) == b . getNonce ( ) . getUint64 ( ) ;
2022-11-13 09:39:26 -04:00
}
// Checks if a Cipher State has an encryption key set
protected hasKey ( ) : boolean {
2022-11-16 11:22:56 -04:00
return ! isEmptyKey ( this . k ) ;
2022-11-13 09:39:26 -04:00
}
// 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
encryptWithAd ( ad : Uint8Array , plaintext : Uint8Array ) : Uint8Array {
this . n . assertValue ( ) ;
let ciphertext = new Uint8Array ( ) ;
if ( this . hasKey ( ) ) {
// If an encryption key is set in the Cipher state, we proceed with encryption
2022-11-15 17:56:25 -04:00
ciphertext = chaCha20Poly1305Encrypt ( plaintext , this . n . getBytes ( ) , ad , this . k ) ;
2022-11-13 09:39:26 -04:00
this . n . increment ( ) ;
this . n . assertValue ( ) ;
2022-12-16 16:05:35 -04:00
log ( "encryptWithAd" , ciphertext , this . n . getUint64 ( ) - 1 ) ;
2022-11-13 09:39:26 -04:00
} else {
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
ciphertext = plaintext ;
2022-12-16 16:05:35 -04:00
log ( "encryptWithAd called with no encryption key set. Returning plaintext." ) ;
2022-11-13 09:39:26 -04:00
}
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
decryptWithAd ( ad : Uint8Array , ciphertext : Uint8Array ) : Uint8Array {
this . n . assertValue ( ) ;
if ( this . hasKey ( ) ) {
2022-11-15 17:56:25 -04:00
const plaintext = chaCha20Poly1305Decrypt ( ciphertext , this . n . getBytes ( ) , ad , this . k ) ;
2022-11-13 09:39:26 -04:00
if ( ! plaintext ) {
2022-11-15 17:56:25 -04:00
throw new Error ( "decryptWithAd failed" ) ;
2022-11-13 09:39:26 -04:00
}
2022-11-14 14:46:42 -04:00
this . n . increment ( ) ;
this . n . assertValue ( ) ;
2022-11-13 09:39:26 -04:00
return plaintext ;
} else {
// Otherwise we return the input ciphertext according to specification
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object
2022-12-16 16:05:35 -04:00
log ( "decryptWithAd called with no encryption key set. Returning ciphertext." ) ;
2022-11-13 09:39:26 -04:00
return ciphertext ;
}
}
// Sets the nonce of a Cipher State
setNonce ( nonce : Nonce ) : void {
this . n = nonce ;
}
// Sets the key of a Cipher State
setCipherStateKey ( key : bytes32 ) : void {
this . k = key ;
}
// Gets the key of a Cipher State
getKey ( ) : bytes32 {
return this . k ;
}
// Gets the nonce of a Cipher State
getNonce ( ) : Nonce {
return this . n ;
}
}
function hashProtocol ( name : string ) : Uint8Array {
// If protocol_name is less than or equal to HASHLEN bytes in length,
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
// Otherwise sets h = HASH(protocol_name).
const protocolName = uint8ArrayFromString ( name , "utf-8" ) ;
if ( protocolName . length <= 32 ) {
const h = new Uint8Array ( 32 ) ;
h . set ( protocolName ) ;
return h ;
} else {
return hashSHA256 ( protocolName ) ;
}
}
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h
export class SymmetricState {
cs : CipherState ;
h : bytes32 ; // handshake hash
2022-12-03 09:37:39 -04:00
private ck : bytes32 ; // chaining key
2022-11-13 09:39:26 -04:00
2022-12-03 09:37:39 -04:00
constructor ( private readonly hsPattern : HandshakePattern ) {
2022-11-13 09:39:26 -04:00
this . h = hashProtocol ( hsPattern . name ) ;
this . ck = this . h ;
this . cs = new CipherState ( ) ;
2022-11-15 17:56:25 -04:00
this . hsPattern = hsPattern ;
}
equals ( b : SymmetricState ) : boolean {
return (
this . cs . equals ( b . cs ) &&
uint8ArrayEquals ( this . ck , b . ck ) &&
uint8ArrayEquals ( this . h , b . h ) &&
this . hsPattern . equals ( b . hsPattern )
) ;
}
clone ( ) : SymmetricState {
const ss = new SymmetricState ( this . hsPattern ) ;
ss . cs = this . cs . clone ( ) ;
ss . ck = new Uint8Array ( this . ck ) ;
ss . h = new Uint8Array ( this . h ) ;
return ss ;
2022-11-13 09:39:26 -04:00
}
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Updates a Symmetric state chaining key and symmetric state
mixKey ( inputKeyMaterial : Uint8Array ) : void {
// We derive two keys using HKDF
const [ ck , tempK ] = getHKDF ( this . ck , inputKeyMaterial ) ;
// We update ck and the Cipher state's key k using the output of HDKF
this . cs = new CipherState ( tempK ) ;
this . ck = ck ;
2022-12-16 16:05:35 -04:00
log ( "mixKey" , this . ck , this . cs . k ) ;
2022-11-13 09:39:26 -04:00
}
// 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
mixHash ( data : Uint8Array ) : void {
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
2022-11-15 17:56:25 -04:00
this . h = hashSHA256 ( uint8ArrayConcat ( [ this . h , data ] ) ) ;
2022-12-16 16:05:35 -04:00
log ( "mixHash" , this . h ) ;
2022-11-13 09:39:26 -04:00
}
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines MixKey and MixHash
mixKeyAndHash ( inputKeyMaterial : Uint8Array ) : void {
// Derives 3 keys using HKDF, the chaining key and the input key material
2022-11-14 14:46:42 -04:00
const [ tmpKey0 , tmpKey1 , tmpKey2 ] = getHKDF ( this . ck , inputKeyMaterial ) ;
2022-11-13 09:39:26 -04:00
// Sets the chaining key
2022-11-14 14:46:42 -04:00
this . ck = tmpKey0 ;
2022-11-13 09:39:26 -04:00
// Updates the handshake hash value
2022-11-14 14:46:42 -04:00
this . mixHash ( tmpKey1 ) ;
2022-11-13 09:39:26 -04:00
// 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."
2022-11-14 14:46:42 -04:00
this . cs = new CipherState ( tmpKey2 ) ;
2022-11-13 09:39:26 -04:00
}
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines encryptWithAd and mixHash
// 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)
2022-11-15 17:56:25 -04:00
encryptAndHash ( plaintext : Uint8Array , extraAd : Uint8Array = new Uint8Array ( ) ) : Uint8Array {
2022-11-13 09:39:26 -04:00
// The additional data
const ad = uint8ArrayConcat ( [ this . h , extraAd ] ) ;
2022-11-14 14:46:42 -04:00
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintext
2022-11-13 09:39:26 -04:00
const ciphertext = this . cs . encryptWithAd ( ad , plaintext ) ;
// We call mixHash over the result
this . mixHash ( ciphertext ) ;
return ciphertext ;
}
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines decryptWithAd and mixHash
2022-11-15 17:56:25 -04:00
decryptAndHash ( ciphertext : Uint8Array , extraAd : Uint8Array = new Uint8Array ( ) ) : Uint8Array {
2022-11-13 09:39:26 -04:00
// The additional data
const ad = uint8ArrayConcat ( [ this . h , extraAd ] ) ;
// Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
const plaintext = this . cs . decryptWithAd ( ad , ciphertext ) ;
// According to specification, the ciphertext enters mixHash (and not the plaintext)
this . 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
split ( ) : { cs1 : CipherState ; cs2 : CipherState } {
// Derives 2 keys using HKDF and the chaining key
2022-11-14 14:46:42 -04:00
const [ tmpKey1 , tmpKey2 ] = getHKDF ( this . ck , new Uint8Array ( 0 ) ) ;
2022-11-13 09:39:26 -04:00
// Returns a tuple of two Cipher States initialized with the derived keys
return {
2022-11-14 14:46:42 -04:00
cs1 : new CipherState ( tmpKey1 ) ,
cs2 : new CipherState ( tmpKey2 ) ,
2022-11-13 09:39:26 -04:00
} ;
}
// Gets the chaining key field of a Symmetric State
getChainingKey ( ) : bytes32 {
return this . ck ;
}
// Gets the handshake hash field of a Symmetric State
getHandshakeHash ( ) : bytes32 {
return this . h ;
}
// Gets the Cipher State field of a Symmetric State
getCipherState ( ) : CipherState {
return this . cs ;
}
}