js-noise/src/noise.ts

371 lines
12 KiB
TypeScript
Raw Normal View History

import { Hash } from "@stablelib/hash";
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";
2023-11-21 12:38:43 -04:00
import { Cipher, hash, HKDF } 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
#################################
*/
2023-01-06 13:34:32 -04:00
/**
* Create empty chaining key
* @returns 32-byte empty key
*/
2022-11-16 11:22:56 -04:00
export function createEmptyKey(): bytes32 {
return new Uint8Array(32);
}
2023-01-06 13:34:32 -04:00
/**
* Checks if a 32-byte key is empty
* @param k key to verify
* @returns true if empty, false otherwise
*/
2022-11-16 11:22:56 -04:00
export function isEmptyKey(k: bytes32): boolean {
const emptyKey = createEmptyKey();
return uint8ArrayEquals(emptyKey, k);
}
2023-01-06 13:34:32 -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)
*/
2022-11-13 09:39:26 -04:00
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;
2023-11-21 12:38:43 -04:00
cipher: Cipher;
2022-11-13 09:39:26 -04:00
2023-01-06 13:34:32 -04:00
/**
* @param k encryption key
* @param n nonce
*/
2023-11-21 12:38:43 -04:00
constructor(cipher: Cipher, 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;
2023-11-21 12:38:43 -04:00
this.cipher = cipher;
2022-11-15 17:56:25 -04:00
}
2023-01-06 13:34:32 -04:00
/**
* Create a copy of the CipherState
* @returns a copy of the CipherState
*/
2022-11-15 17:56:25 -04:00
clone(): CipherState {
2023-11-21 12:38:43 -04:00
return new CipherState(this.cipher, new Uint8Array(this.k), new Nonce(this.n.getUint64()));
2022-11-15 17:56:25 -04:00
}
2023-01-06 13:34:32 -04:00
/**
* Check CipherState equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: CipherState): boolean {
return uint8ArrayEquals(this.k, other.getKey()) && this.n.getUint64() == other.getNonce().getUint64();
2022-11-13 09:39:26 -04:00
}
2023-01-06 13:34:32 -04:00
/**
* Checks if a Cipher State has an encryption key set
* @returns true if a key is set, false otherwise`
*/
2022-11-13 09:39:26 -04:00
protected hasKey(): boolean {
2022-11-16 11:22:56 -04:00
return !isEmptyKey(this.k);
2022-11-13 09:39:26 -04:00
}
2023-01-06 13:34:32 -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
* @param ad associated data
* @param plaintext data to encrypt
*/
2022-11-13 09:39:26 -04:00
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
ciphertext = this.cipher.encrypt(this.k, this.n, ad, plaintext);
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;
}
2023-01-06 13:34:32 -04:00
/**
* 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
* @param ad associated data
* @param ciphertext data to decrypt
*/
2022-11-13 09:39:26 -04:00
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
this.n.assertValue();
if (this.hasKey()) {
const plaintext = this.cipher.decrypt(this.k, this.n, ad, ciphertext);
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;
}
}
2023-01-06 13:34:32 -04:00
/**
* Sets the nonce of a Cipher State
* @param nonce Nonce
*/
2022-11-13 09:39:26 -04:00
setNonce(nonce: Nonce): void {
this.n = nonce;
}
2023-01-06 13:34:32 -04:00
/**
* Sets the key of a Cipher State
* @param key set the cipherstate encryption key
*/
2022-11-13 09:39:26 -04:00
setCipherStateKey(key: bytes32): void {
this.k = key;
}
2023-01-06 13:34:32 -04:00
/**
* Gets the encryption key of a Cipher State
* @returns encryption key
*/
2022-11-13 09:39:26 -04:00
getKey(): bytes32 {
return this.k;
}
2023-01-06 13:34:32 -04:00
/**
* Gets the nonce of a Cipher State
* @returns Nonce
*/
2022-11-13 09:39:26 -04:00
getNonce(): Nonce {
return this.n;
}
}
2023-01-06 13:34:32 -04:00
/**
* Hash protocol name
* @param name name of the noise handshake pattern to hash
* @returns sha256 digest of the protocol name
*/
function hashProtocol(h: new () => Hash, name: string): Uint8Array {
2022-11-13 09:39:26 -04:00
// 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 hash(h, protocolName);
2022-11-13 09:39:26 -04:00
}
}
2023-01-06 13:34:32 -04:00
/**
* 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
*/
2022-11-13 09:39:26 -04:00
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
2023-11-20 14:34:08 -04:00
constructor(private readonly handshakePattern: HandshakePattern) {
this.h = hashProtocol(handshakePattern.hash, handshakePattern.name);
2022-11-13 09:39:26 -04:00
this.ck = this.h;
2023-11-21 12:38:43 -04:00
this.cs = new CipherState(handshakePattern.cipher);
2022-11-15 17:56:25 -04:00
}
2023-01-06 13:34:32 -04:00
/**
* Check CipherState equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: SymmetricState): boolean {
2022-11-15 17:56:25 -04:00
return (
2023-01-06 13:34:32 -04:00
this.cs.equals(other.cs) &&
uint8ArrayEquals(this.ck, other.ck) &&
uint8ArrayEquals(this.h, other.h) &&
2023-11-20 14:34:08 -04:00
this.handshakePattern.equals(other.handshakePattern)
2022-11-15 17:56:25 -04:00
);
}
2023-01-06 13:34:32 -04:00
/**
* Create a copy of the SymmetricState
* @returns a copy of the SymmetricState
*/
2022-11-15 17:56:25 -04:00
clone(): SymmetricState {
2023-11-20 14:34:08 -04:00
const ss = new SymmetricState(this.handshakePattern);
2022-11-15 17:56:25 -04:00
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
}
2023-01-06 13:34:32 -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
* @param inputKeyMaterial
*/
2022-11-13 09:39:26 -04:00
mixKey(inputKeyMaterial: Uint8Array): void {
// We derive two keys using HKDF
2023-11-20 14:34:08 -04:00
const [ck, tempK] = HKDF(this.handshakePattern.hash, this.ck, inputKeyMaterial, 32, 2);
2022-11-13 09:39:26 -04:00
// We update ck and the Cipher state's key k using the output of HDKF
2023-11-21 12:38:43 -04:00
this.cs = new CipherState(this.handshakePattern.cipher, tempK);
2022-11-13 09:39:26 -04:00
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
}
2023-01-06 13:34:32 -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
* @param data input data to hash into h
*/
2022-11-13 09:39:26 -04:00
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
this.h = hash(this.handshakePattern.hash, uint8ArrayConcat([this.h, data]));
2022-12-16 16:05:35 -04:00
log("mixHash", this.h);
2022-11-13 09:39:26 -04:00
}
2023-01-06 13:34:32 -04:00
/**
* mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Combines MixKey and MixHash
* @param inputKeyMaterial
*/
2022-11-13 09:39:26 -04:00
mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
// Derives 3 keys using HKDF, the chaining key and the input key material
2023-11-20 14:34:08 -04:00
const [tmpKey0, tmpKey1, tmpKey2] = HKDF(this.handshakePattern.hash, this.ck, inputKeyMaterial, 32, 3);
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."
2023-11-21 12:38:43 -04:00
this.cs = new CipherState(this.handshakePattern.cipher, tmpKey2);
2022-11-13 09:39:26 -04:00
}
2023-01-06 13:34:32 -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)
* @param plaintext data to encrypt
* @param extraAd extra additional data
*/
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;
}
2023-01-06 13:34:32 -04:00
/**
* DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Combines decryptWithAd and mixHash
* @param ciphertext data to decrypt
* @param extraAd extra additional data
*/
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;
}
2023-01-06 13:34:32 -04:00
/**
* 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
* @returns CipherState to encrypt and CipherState to decrypt
*/
2022-11-13 09:39:26 -04:00
split(): { cs1: CipherState; cs2: CipherState } {
// Derives 2 keys using HKDF and the chaining key
2023-11-20 14:34:08 -04:00
const [tmpKey1, tmpKey2] = HKDF(this.handshakePattern.hash, this.ck, new Uint8Array(0), 32, 2);
2022-11-13 09:39:26 -04:00
// Returns a tuple of two Cipher States initialized with the derived keys
return {
2023-11-21 12:38:43 -04:00
cs1: new CipherState(this.handshakePattern.cipher, tmpKey1),
cs2: new CipherState(this.handshakePattern.cipher, tmpKey2),
2022-11-13 09:39:26 -04:00
};
}
2023-01-06 13:34:32 -04:00
/**
* Gets the chaining key field of a Symmetric State
* @returns Chaining key
*/
2022-11-13 09:39:26 -04:00
getChainingKey(): bytes32 {
return this.ck;
}
2023-01-06 13:34:32 -04:00
/**
* Gets the handshake hash field of a Symmetric State
* @returns Handshake hash
*/
2022-11-13 09:39:26 -04:00
getHandshakeHash(): bytes32 {
return this.h;
}
2023-01-06 13:34:32 -04:00
/**
* Gets the Cipher State field of a Symmetric State
* @returns Cipher State
*/
2022-11-13 09:39:26 -04:00
getCipherState(): CipherState {
return this.cs;
}
}