test: handshakes

This commit is contained in:
Richard Ramos 2022-11-15 17:56:25 -04:00
parent 11b97bafaf
commit 031c9e073b
No known key found for this signature in database
GPG Key ID: BD36D48BC9FFC88C
15 changed files with 890 additions and 335 deletions

View File

@ -2,7 +2,21 @@
"version": "0.1",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en",
"words": ["Waku", "keypair", "nwaku", "Nametag, ciphertext", "unpad", "blocksize", "Nametag", "Cipherstate", "Nametags", "HASHLEN", "ciphertext", "preshared", "libp2p"],
"words": [
"Waku",
"keypair",
"nwaku",
"Nametag, ciphertext",
"unpad",
"blocksize",
"Nametag",
"Cipherstate",
"Nametags",
"HASHLEN",
"ciphertext",
"preshared",
"libp2p"
],
"flagWords": [],
"ignorePaths": [
"package.json",

View File

@ -22,6 +22,12 @@
"WebAssembly": true
},
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 120
}
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{

View File

@ -3,9 +3,6 @@
"spec": "src/**/*.spec.ts",
"require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
"loader": "ts-node/esm",
"node-option": [
"experimental-specifier-resolution=node",
"loader=ts-node/esm"
],
"node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm"],
"exit": true
}

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@ -1,7 +1,7 @@
module.exports = [
{
name: "RLN core",
name: "JS-Noice core",
path: "bundle/index.js",
import: "{ RLN }",
import: "{ Noise }",
},
];

View File

@ -5,9 +5,7 @@ const path = require("path");
const ResolveTypeScriptPlugin = require("resolve-typescript-plugin");
const output = {
path:
path.join(os.tmpdir(), "_karma_webpack_") +
Math.floor(Math.random() * 1000000),
path: path.join(os.tmpdir(), "_karma_webpack_") + Math.floor(Math.random() * 1000000),
};
module.exports = function (config) {

View File

@ -15,7 +15,7 @@ export function hashSHA256(data: Uint8Array): Uint8Array {
export function intoCurve25519Key(s: Uint8Array): bytes32 {
if (s.length != x25519.PUBLIC_KEY_LENGTH) {
throw "invalid public key length";
throw new Error("invalid public key length");
}
return s;
@ -51,19 +51,11 @@ export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
};
}
export function generateX25519SharedKey(
privateKey: Uint8Array,
publicKey: Uint8Array
): Uint8Array {
export function generateX25519SharedKey(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
return x25519.sharedKey(privateKey, publicKey);
}
export function chaCha20Poly1305Encrypt(
plaintext: Uint8Array,
nonce: Uint8Array,
ad: Uint8Array,
k: bytes32
): bytes {
export function chaCha20Poly1305Encrypt(plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
const ctx = new ChaCha20Poly1305(k);
return ctx.seal(nonce, plaintext, ad);

View File

@ -1,4 +1,5 @@
import * as pkcs7 from "pkcs7-padding";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic";
import { KeyPair } from "./@types/keypair";
@ -17,6 +18,17 @@ import { NoisePublicKey } from "./publickey";
export class HandshakeStepResult {
payload2: PayloadV2 = new PayloadV2();
transportMessage: Uint8Array = new Uint8Array();
equals(b: HandshakeStepResult): boolean {
return this.payload2.equals(b.payload2) && uint8ArrayEquals(this.transportMessage, b.transportMessage);
}
clone(): HandshakeStepResult {
const r = new HandshakeStepResult();
r.transportMessage = new Uint8Array(this.transportMessage);
r.payload2 = this.payload2.clone();
return r;
}
}
// When a handshake is complete, the HandshakeResult will contain the two
@ -31,20 +43,95 @@ export class HandshakeResult {
nametagsOutbound: MessageNametagBuffer = new MessageNametagBuffer();
rs: bytes32 = new Uint8Array();
h: bytes32 = new Uint8Array();
// Noise specification, Section 5:
// Transport messages are then encrypted and decrypted by calling EncryptWithAd()
// and DecryptWithAd() on the relevant CipherState with zero-length associated data.
// If DecryptWithAd() signals an error due to DECRYPT() failure, then the input message is discarded.
// The application may choose to delete the CipherState and terminate the session on such an error,
// or may continue to attempt communications. If EncryptWithAd() or DecryptWithAd() signal an error
// due to nonce exhaustion, then the application must delete the CipherState and terminate the session.
// Writes an encrypted message using the proper Cipher State
writeMessage(transportMessage: Uint8Array, outboundMessageNametagBuffer: MessageNametagBuffer): PayloadV2 {
const payload2 = new PayloadV2();
// We set the message nametag using the input buffer
payload2.messageNametag = outboundMessageNametagBuffer.pop();
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
// This correspond to setting protocol-id to 0
payload2.protocolId = 0;
// We pad the transport message
const paddedTransportMessage = pkcs7.pad(transportMessage, NoisePaddingBlockSize);
// Encryption is done with zero-length associated data as per specification
payload2.transportMessage = this.csOutbound!.encryptWithAd(payload2.messageNametag, paddedTransportMessage);
return payload2;
}
// Reads an encrypted message using the proper Cipher State
// Decryption is attempted only if the input PayloadV2 has a messageNametag equal to the one expected
readMessage(readPayload2: PayloadV2, inboundMessageNametagBuffer: MessageNametagBuffer): Uint8Array {
// The output decrypted message
let message = new Uint8Array();
// If the message nametag does not correspond to the nametag expected in the inbound message nametag buffer
// an error is raised (to be handled externally, i.e. re-request lost messages, discard, etc.)
const nametagIsOk = inboundMessageNametagBuffer.checkNametag(readPayload2.messageNametag);
if (!nametagIsOk) {
throw new Error("nametag is not ok");
}
// At this point the messageNametag matches the expected nametag.
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
if (readPayload2.protocolId == 0) {
// On application level we decide to discard messages which fail decryption, without raising an error
try {
// Decryption is done with messageNametag as associated data
const paddedMessage = this.csInbound!.decryptWithAd(readPayload2.messageNametag, readPayload2.transportMessage);
// We unpad the decrypted message
message = pkcs7.unpad(paddedMessage);
// The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer
inboundMessageNametagBuffer.delete(1);
} catch (err) {
console.debug("A read message failed decryption. Returning empty message as plaintext.");
message = new Uint8Array();
}
}
return message;
}
}
export interface HandshakeParameters {
hsPattern: HandshakePattern;
ephemeralKey?: KeyPair;
staticKey?: KeyPair;
prologue?: Uint8Array;
psk?: Uint8Array;
preMessagePKs?: Array<NoisePublicKey>;
initiator?: boolean;
}
export interface StepHandshakeParameters {
readPayloadV2?: PayloadV2;
transportMessage?: Uint8Array;
messageNametag?: Uint8Array;
}
export class Handshake {
hs: HandshakeState;
constructor(
hasPattern: HandshakePattern,
ephemeralKey: KeyPair,
staticKey?: KeyPair,
prologue: Uint8Array = new Uint8Array(),
psk: Uint8Array = new Uint8Array(),
preMessagePKs: Array<NoisePublicKey> = [],
initiator = false
) {
this.hs = new HandshakeState(hasPattern, psk);
constructor({
hsPattern,
ephemeralKey,
staticKey,
prologue = new Uint8Array(),
psk = new Uint8Array(),
preMessagePKs = [],
initiator = false,
}: HandshakeParameters) {
this.hs = new HandshakeState(hsPattern, psk);
this.hs.ss.mixHash(prologue);
this.hs.e = ephemeralKey;
this.hs.s = staticKey;
@ -56,34 +143,40 @@ export class Handshake {
this.hs.processPreMessagePatternTokens(preMessagePKs);
}
equals(b: Handshake): boolean {
return this.hs.equals(b.hs);
}
clone(): Handshake {
const result = new Handshake({
hsPattern: this.hs.handshakePattern,
});
result.hs = this.hs.clone();
return result;
}
// Advances 1 step in handshake
// Each user in a handshake alternates writing and reading of handshake messages.
// If the user is writing the handshake message, the transport message (if not empty) and eventually a non-empty message nametag has to be passed to transportMessage and messageNametag and readPayloadV2 can be left to its default value
// It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. Decryption is skipped if the PayloadV2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
stepHandshake(
readPayloadV2: PayloadV2 = new PayloadV2(),
transportMessage: Uint8Array = new Uint8Array(),
messageNametag: Uint8Array = new Uint8Array()
): HandshakeStepResult {
stepHandshake({
readPayloadV2 = new PayloadV2(),
transportMessage = new Uint8Array(),
messageNametag = new Uint8Array(),
}: StepHandshakeParameters): HandshakeStepResult {
const hsStepResult = new HandshakeStepResult();
// If there are no more message patterns left for processing
// we return an empty HandshakeStepResult
if (
this.hs.msgPatternIdx >
this.hs.handshakePattern.messagePatterns.length - 1
) {
console.debug(
"stepHandshake called more times than the number of message patterns present in handshake"
);
if (this.hs.msgPatternIdx > this.hs.handshakePattern.messagePatterns.length - 1) {
console.debug("stepHandshake called more times than the number of message patterns present in handshake");
return hsStepResult;
}
// We process the next handshake message pattern
// We get if the user is reading or writing the input handshake message
const direction =
this.hs.handshakePattern.messagePatterns[this.hs.msgPatternIdx].direction;
const direction = this.hs.handshakePattern.messagePatterns[this.hs.msgPatternIdx].direction;
const { reading, writing } = this.hs.getReadingWritingState(direction);
// If we write an answer at this handshake step
@ -91,20 +184,17 @@ export class Handshake {
// We initialize a payload v2 and we set proper protocol ID (if supported)
try {
hsStepResult.payload2.protocolId =
PayloadV2ProtocolIDs[
this.hs.handshakePattern.name as keyof typeof PayloadV2ProtocolIDs
];
PayloadV2ProtocolIDs[this.hs.handshakePattern.name as keyof typeof PayloadV2ProtocolIDs];
} catch (err) {
throw "Handshake Pattern not supported";
throw new Error("handshake pattern not supported");
}
// We set the messageNametag and the handshake and transport messages
hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag);
hsStepResult.payload2.handshakeMessage =
this.hs.processMessagePatternTokens();
hsStepResult.payload2.handshakeMessage = this.hs.processMessagePatternTokens();
// We write the payload by passing the messageNametag as extra additional data
hsStepResult.payload2.transportMessage =
this.hs.processMessagePatternPayload(
hsStepResult.payload2.transportMessage = this.hs.processMessagePatternPayload(
transportMessage,
hsStepResult.payload2.messageNametag
);
@ -112,8 +202,8 @@ export class Handshake {
// If we read an answer during this handshake step
} else if (reading) {
// If the read message nametag doesn't match the expected input one we raise an error
if (readPayloadV2.messageNametag != toMessageNametag(messageNametag)) {
throw "The message nametag of the read message doesn't match the expected one";
if (!uint8ArrayEquals(readPayloadV2.messageNametag, toMessageNametag(messageNametag))) {
throw new Error("the message nametag of the read message doesn't match the expected one");
}
// We process the read public keys and (eventually decrypt) the read transport message
@ -128,7 +218,7 @@ export class Handshake {
readPayloadV2.messageNametag
);
} else {
throw "Handshake Error: neither writing or reading user";
throw new Error("handshake Error: neither writing or reading user");
}
// We increase the handshake state message pattern index to progress to next step
@ -177,84 +267,4 @@ export class Handshake {
return hsResult;
}
// Noise specification, Section 5:
// Transport messages are then encrypted and decrypted by calling EncryptWithAd()
// and DecryptWithAd() on the relevant CipherState with zero-length associated data.
// If DecryptWithAd() signals an error due to DECRYPT() failure, then the input message is discarded.
// The application may choose to delete the CipherState and terminate the session on such an error,
// or may continue to attempt communications. If EncryptWithAd() or DecryptWithAd() signal an error
// due to nonce exhaustion, then the application must delete the CipherState and terminate the session.
// Writes an encrypted message using the proper Cipher State
writeMessage(
hsr: HandshakeResult,
transportMessage: Uint8Array,
outboundMessageNametagBuffer: MessageNametagBuffer
): PayloadV2 {
const payload2 = new PayloadV2();
// We set the message nametag using the input buffer
payload2.messageNametag = outboundMessageNametagBuffer.pop();
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
// This correspond to setting protocol-id to 0
payload2.protocolId = 0;
// We pad the transport message
const paddedTransportMessage = pkcs7.pad(
transportMessage,
NoisePaddingBlockSize
);
// Encryption is done with zero-length associated data as per specification
payload2.transportMessage = hsr.csOutbound!.encryptWithAd(
payload2.messageNametag,
paddedTransportMessage
);
return payload2;
}
// Reads an encrypted message using the proper Cipher State
// Decryption is attempted only if the input PayloadV2 has a messageNametag equal to the one expected
readMessage(
hsr: HandshakeResult,
readPayload2: PayloadV2,
inboundMessageNametagBuffer: MessageNametagBuffer
): Uint8Array {
// The output decrypted message
let message = new Uint8Array();
// If the message nametag does not correspond to the nametag expected in the inbound message nametag buffer
// an error is raised (to be handled externally, i.e. re-request lost messages, discard, etc.)
const nametagIsOk = inboundMessageNametagBuffer.checkNametag(
readPayload2.messageNametag
);
if (!nametagIsOk) {
throw "nametag is not ok";
}
// At this point the messageNametag matches the expected nametag.
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
if (readPayload2.protocolId == 0) {
// On application level we decide to discard messages which fail decryption, without raising an error
try {
// Decryption is done with messageNametag as associated data
const paddedMessage = hsr.csInbound!.decryptWithAd(
readPayload2.messageNametag,
readPayload2.transportMessage
);
// We unpad the decrypted message
message = pkcs7.unpad(paddedMessage);
// The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer
inboundMessageNametagBuffer.delete(1);
} catch (err) {
console.debug(
"A read message failed decryption. Returning empty message as plaintext."
);
message = new Uint8Array();
}
}
return message;
}
}

View File

@ -1,22 +1,11 @@
import * as pkcs7 from "pkcs7-padding";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic.js";
import type { KeyPair } from "./@types/keypair.js";
import {
Curve25519KeySize,
dh,
generateX25519KeyPair,
getHKDF,
intoCurve25519Key,
} from "./crypto.js";
import { Curve25519KeySize, dh, generateX25519KeyPair, getHKDF, intoCurve25519Key } from "./crypto.js";
import { SymmetricState } from "./noise.js";
import {
EmptyPreMessage,
HandshakePattern,
MessageDirection,
NoiseTokens,
PreMessagePattern,
} from "./patterns";
import { EmptyPreMessage, HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns";
import { NoisePublicKey } from "./publickey.js";
// The padding blocksize of a transport message
@ -54,6 +43,58 @@ export class HandshakeState {
this.msgPatternIdx = 0;
}
equals(b: HandshakeState): boolean {
if (this.s != null && b.s != null) {
if (!uint8ArrayEquals(this.s.privateKey, b.s.privateKey)) return false;
if (!uint8ArrayEquals(this.s.publicKey, b.s.publicKey)) return false;
} else {
return false;
}
if (this.e != null && b.e != null) {
if (!uint8ArrayEquals(this.e.privateKey, b.e.privateKey)) return false;
if (!uint8ArrayEquals(this.e.publicKey, b.e.publicKey)) return false;
} else {
return false;
}
if (this.rs != null && b.rs != null) {
if (!uint8ArrayEquals(this.rs, b.rs)) return false;
} else {
return false;
}
if (this.re != null && b.re != null) {
if (!uint8ArrayEquals(this.re, b.re)) return false;
} else {
return false;
}
if (!this.ss.equals(b.ss)) return false;
if (this.initiator != b.initiator) return false;
if (!this.handshakePattern.equals(b.handshakePattern)) return false;
if (this.msgPatternIdx != b.msgPatternIdx) return false;
if (!uint8ArrayEquals(this.psk, b.psk)) return false;
return true;
}
clone(): HandshakeState {
const result = new HandshakeState(this.handshakePattern, this.psk);
result.s = this.s;
result.e = this.e;
result.rs = this.rs;
result.re = this.re;
result.ss = this.ss.clone();
result.initiator = this.initiator;
result.msgPatternIdx = this.msgPatternIdx;
return result;
}
genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } {
const [nms1, nms2] = getHKDF(this.ss.h, new Uint8Array());
return { nms1, nms2 };
@ -119,11 +160,9 @@ export class HandshakeState {
// Handshake messages processing procedures
// Processes pre-message patterns
processPreMessagePatternTokens(
inPreMessagePKs: Array<NoisePublicKey> = []
): void {
processPreMessagePatternTokens(inPreMessagePKs: Array<NoisePublicKey> = []): void {
// I make a copy of the input pre-message public keys, so that I can easily delete processed ones without using iterators/counters
const preMessagePKs = inPreMessagePKs;
const preMessagePKs = inPreMessagePKs.map((x) => x.clone());
// Here we store currently processed pre message public key
let currPK: NoisePublicKey;
@ -136,7 +175,7 @@ export class HandshakeState {
// If not empty, we check that pre-message is valid according to Noise specifications
if (!this.isValid(this.handshakePattern.preMessagePatterns)) {
throw "invalid pre-message in handshake";
throw new Error("invalid pre-message in handshake");
}
// We iterate over each pattern contained in the pre-message
@ -156,7 +195,7 @@ export class HandshakeState {
if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0];
} else {
throw "noise pre-message read e, expected a public key";
throw new Error("noise pre-message read e, expected a public key");
}
// If user is reading the "e" token
@ -169,7 +208,7 @@ export class HandshakeState {
this.re = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.re);
} else {
throw "noise read e, incorrect encryption flag for pre-message public key";
throw new Error("noise read e, incorrect encryption flag for pre-message public key");
}
// If user is writing the "e" token
} else if (writing) {
@ -177,10 +216,10 @@ export class HandshakeState {
// When writing, the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(e.public_key).
if (this.e && this.e.publicKey == intoCurve25519Key(currPK.pk)) {
if (this.e && uint8ArrayEquals(this.e.publicKey, intoCurve25519Key(currPK.pk))) {
this.ss.mixHash(this.e.publicKey);
} else {
throw "noise pre-message e key doesn't correspond to locally set e key pair";
throw new Error("noise pre-message e key doesn't correspond to locally set e key pair");
}
}
@ -200,7 +239,7 @@ export class HandshakeState {
if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0];
} else {
throw "noise pre-message read s, expected a public key";
throw new Error("noise pre-message read s, expected a public key");
}
// If user is reading the "s" token
@ -213,7 +252,7 @@ export class HandshakeState {
this.rs = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.rs);
} else {
throw "noise read s, incorrect encryption flag for pre-message public key";
throw new Error("noise read s, incorrect encryption flag for pre-message public key");
}
// If user is writing the "s" token
@ -222,10 +261,10 @@ export class HandshakeState {
// If writing, it means that the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(s.public_key).
if (this.s && this.s.publicKey == intoCurve25519Key(currPK.pk)) {
if (this.s && uint8ArrayEquals(this.s.publicKey, intoCurve25519Key(currPK.pk))) {
this.ss.mixHash(this.s.publicKey);
} else {
throw "noise pre-message s key doesn't correspond to locally set s key pair";
throw new Error("noise pre-message s key doesn't correspond to locally set s key pair");
}
}
@ -241,7 +280,7 @@ export class HandshakeState {
preMessagePKs.shift();
break;
default:
throw "invalid Token for pre-message pattern";
throw new Error("invalid Token for pre-message pattern");
}
}
}
@ -249,15 +288,11 @@ export class HandshakeState {
// This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
// An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
processMessagePatternPayload(
transportMessage: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
processMessagePatternPayload(transportMessage: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
let payload: Uint8Array;
// We retrieve current message pattern (direction + tokens) to process
const direction =
this.handshakePattern.messagePatterns[this.msgPatternIdx].direction;
const direction = this.handshakePattern.messagePatterns[this.msgPatternIdx].direction;
// We get if the user is reading or writing the input handshake message
const { reading, writing } = this.getReadingWritingState(direction);
@ -265,23 +300,20 @@ export class HandshakeState {
// We decrypt the transportMessage, if any
if (reading) {
payload = this.ss.decryptAndHash(transportMessage, extraAd);
payload = pkcs7.pad(payload, NoisePaddingBlockSize);
payload = pkcs7.unpad(payload);
} else if (writing) {
payload = pkcs7.unpad(transportMessage);
payload = pkcs7.pad(transportMessage, NoisePaddingBlockSize);
payload = this.ss.encryptAndHash(payload, extraAd);
} else {
throw "undefined state";
throw new Error("undefined state");
}
return payload;
}
// We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
processMessagePatternTokens(
inputHandshakeMessage: Array<NoisePublicKey> = []
): Array<NoisePublicKey> {
processMessagePatternTokens(inputHandshakeMessage: Array<NoisePublicKey> = []): Array<NoisePublicKey> {
// We retrieve current message pattern (direction + tokens) to process
const messagePattern =
this.handshakePattern.messagePatterns[this.msgPatternIdx];
const messagePattern = this.handshakePattern.messagePatterns[this.msgPatternIdx];
const direction = messagePattern.direction;
const tokens = messagePattern.tokens;
@ -311,7 +343,7 @@ export class HandshakeState {
if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0];
} else {
throw "noise read e, expected a public key";
throw new Error("noise read e, expected a public key");
}
// We check if current key is encrypted or not
@ -328,7 +360,7 @@ export class HandshakeState {
// Decrypts re, sets re and calls MixHash(re.public_key).
this.re = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else {
throw "noise read e, incorrect encryption flag for public key";
throw new Error("noise read e, incorrect encryption flag for public key");
}
// Noise specification: section 9.2
@ -374,7 +406,7 @@ export class HandshakeState {
if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0];
} else {
throw "noise read s, expected a public key";
throw new Error("noise read s, expected a public key");
}
// We check if current key is encrypted or not
@ -388,7 +420,7 @@ export class HandshakeState {
// Decrypts rs, sets rs and calls MixHash(rs.public_key).
this.rs = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else {
throw "noise read s, incorrect encryption flag for public key";
throw new Error("noise read s, incorrect encryption flag for public key");
}
// We delete processed public key
@ -400,7 +432,7 @@ export class HandshakeState {
// If the local static key is not set (the handshake state was not properly initialized), we raise an error
if (!this.s) {
throw "static key not set";
throw new Error("static key not set");
}
// We encrypt the public part of the static key in case a key is set in the Cipher State
@ -435,7 +467,7 @@ export class HandshakeState {
// If local and/or remote ephemeral keys are not set, we raise an error
if (!this.e || !this.re) {
throw "local or remote ephemeral key not set";
throw new Error("local or remote ephemeral key not set");
}
// Calls MixKey(DH(e, re)).
@ -451,13 +483,13 @@ export class HandshakeState {
// If both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
if (this.initiator) {
if (!this.e || !this.rs) {
throw "local or remote ephemeral/static key not set";
throw new Error("local or remote ephemeral/static key not set");
}
this.ss.mixKey(dh(this.e.privateKey, this.rs));
} else {
if (!this.re || !this.s) {
throw "local or remote ephemeral/static key not set";
throw new Error("local or remote ephemeral/static key not set");
}
this.ss.mixKey(dh(this.s.privateKey, this.re));
@ -473,13 +505,13 @@ export class HandshakeState {
// If both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
if (this.initiator) {
if (!this.s || !this.re) {
throw "local or remote ephemeral/static key not set";
throw new Error("local or remote ephemeral/static key not set");
}
this.ss.mixKey(dh(this.s.privateKey, this.re));
} else {
if (!this.rs || !this.e) {
throw "local or remote ephemeral/static key not set";
throw new Error("local or remote ephemeral/static key not set");
}
this.ss.mixKey(dh(this.e.privateKey, this.rs));
@ -493,7 +525,7 @@ export class HandshakeState {
// If local and/or remote static keys are not set, we raise an error
if (!this.s || !this.rs) {
throw "local or remote static key not set";
throw new Error("local or remote static key not set");
}
// Calls MixKey(DH(s, rs)).

View File

@ -4,8 +4,12 @@ import { expect } from "chai";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
import { CipherState } from "./noise";
import { Handshake, HandshakeStepResult } from "./handshake";
import { CipherState, SymmetricState } from "./noise";
import { MAX_NONCE, Nonce } from "./nonce";
import { NoiseHandshakePatterns } from "./patterns";
import { MessageNametagBuffer } from "./payload";
import { NoisePublicKey } from "./publickey";
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
const randomCipherState = new CipherState();
@ -14,7 +18,13 @@ function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
return randomCipherState;
}
function c(input: Uint8Array): Uint8Array {
return new Uint8Array(input);
}
describe("js-noise", () => {
const rng = new HMACDRBG();
it("Noise State Machine: Diffie-Hellman operation", function () {
const aliceKey = generateX25519KeyPair();
const bobKey = generateX25519KeyPair();
@ -28,8 +38,6 @@ describe("js-noise", () => {
});
it("Noise State Machine: Cipher State primitives", function () {
const rng = new HMACDRBG();
// We generate a random Cipher State, associated data ad and plaintext
let cipherState = randomCipherState(rng);
let nonceValue = Math.floor(Math.random() * MAX_NONCE);
@ -104,12 +112,7 @@ describe("js-noise", () => {
plaintext = randomBytes(128, rng);
// We perform encryption using the Cipher State key, NonceMax and ad
ciphertext = chaCha20Poly1305Encrypt(
plaintext,
cipherState.getNonce().getBytes(),
ad,
cipherState.getKey()
);
ciphertext = chaCha20Poly1305Encrypt(plaintext, cipherState.getNonce().getBytes(), ad, cipherState.getKey());
// At this point ciphertext is a proper encryption of the original plaintext obtained with nonce equal to NonceMax
// We can now test if decryption fails with a NoiseNonceMaxError error. Any subsequent decryption call over the Cipher State should fail similarly and leave the nonce unchanged
@ -125,4 +128,503 @@ describe("js-noise", () => {
expect(cipherState.getNonce().getUint64()).to.be.equals(MAX_NONCE + 1);
}
});
it("Noise State Machine: Cipher State primitives", function () {
// We select one supported handshake pattern and we initialize a symmetric state
const hsPattern = NoiseHandshakePatterns.XX;
let symmetricState = new SymmetricState(hsPattern);
// We get all the Symmetric State field
let cs = symmetricState.getCipherState().clone(); // Cipher State
let ck = c(symmetricState.getChainingKey()); // chaining key
let h = c(symmetricState.getHandshakeHash()); // handshake hash
// When a Symmetric state is initialized, handshake hash and chaining key are (byte-wise) equal
expect(uint8ArrayEquals(h, ck)).to.be.true;
// mixHash
// ==========
// We generate a random byte sequence and execute a mixHash over it
symmetricState.mixHash(rng.randomBytes(128));
// mixHash changes only the handshake hash value of the Symmetric state
expect(cs.equals(symmetricState.getCipherState())).to.be.true;
expect(uint8ArrayEquals(symmetricState.getChainingKey(), ck)).to.be.true;
expect(uint8ArrayEquals(symmetricState.getHandshakeHash(), h)).to.be.false;
// We update test values
h = c(symmetricState.getHandshakeHash());
// mixKey
// ==========
// We generate random input key material and we execute mixKey
let inputKeyMaterial = rng.randomBytes(128);
symmetricState.mixKey(inputKeyMaterial);
// mixKey changes the Symmetric State's chaining key and encryption key of the embedded Cipher State
// It further sets to 0 the nonce of the embedded Cipher State
expect(uint8ArrayEquals(cs.getKey(), symmetricState.cs.getKey())).to.be.false;
expect(symmetricState.getCipherState().getNonce().equals(new Nonce())).to.be.true;
expect(cs.equals(symmetricState.getCipherState())).to.be.false;
expect(uint8ArrayEquals(symmetricState.getChainingKey(), ck)).to.be.false;
expect(uint8ArrayEquals(symmetricState.getHandshakeHash(), h)).to.be.true;
// We update test values
cs = symmetricState.getCipherState().clone();
ck = c(symmetricState.getChainingKey());
// mixKeyAndHash
// ==========
// We generate random input key material and we execute mixKeyAndHash
inputKeyMaterial = rng.randomBytes(128);
symmetricState.mixKeyAndHash(inputKeyMaterial);
// mixKeyAndHash executes a mixKey and a mixHash using the input key material
// All Symmetric State's fields are updated
expect(cs.equals(symmetricState.getCipherState())).to.be.false;
expect(uint8ArrayEquals(symmetricState.getChainingKey(), ck)).to.be.false;
expect(uint8ArrayEquals(symmetricState.getHandshakeHash(), h)).to.be.false;
// We update test values
cs = symmetricState.getCipherState().clone();
ck = c(symmetricState.getChainingKey());
h = c(symmetricState.getHandshakeHash());
// encryptAndHash and decryptAndHash
// =========
// We store the initial symmetricState in order to correctly perform decryption
const initialSymmetricState = symmetricState.clone();
// We generate random plaintext and we execute encryptAndHash
const plaintext = rng.randomBytes(128);
const nonce = symmetricState.getCipherState().getNonce().clone();
const ciphertext = symmetricState.encryptAndHash(plaintext);
// encryptAndHash combines encryptWithAd and mixHash over the ciphertext (encryption increases the nonce of the embedded Cipher State but does not change its key)
// We check if only the handshake hash value and the Symmetric State changed accordingly
expect(cs.equals(symmetricState.getCipherState())).to.be.false;
expect(uint8ArrayEquals(cs.getKey(), symmetricState.getCipherState().getKey())).to.be.true;
expect(symmetricState.getCipherState().getNonce().getUint64()).to.be.equals(nonce.getUint64() + 1);
expect(uint8ArrayEquals(ck, symmetricState.getChainingKey())).to.be.true;
expect(uint8ArrayEquals(h, symmetricState.getHandshakeHash())).to.be.false;
// We restore the symmetric State to its initial value to test decryption
symmetricState = initialSymmetricState;
// We execute decryptAndHash over the ciphertext
const decrypted = symmetricState.decryptAndHash(ciphertext);
// decryptAndHash combines decryptWithAd and mixHash over the ciphertext (encryption increases the nonce of the embedded Cipher State but does not change its key)
// We check if only the handshake hash value and the Symmetric State changed accordingly
// We further check if decryption corresponds to the original plaintext
expect(cs.equals(symmetricState.getCipherState())).to.be.false;
expect(uint8ArrayEquals(cs.getKey(), symmetricState.getCipherState().getKey())).to.be.true;
expect(symmetricState.getCipherState().getNonce().getUint64()).to.be.equals(nonce.getUint64() + 1);
expect(uint8ArrayEquals(ck, symmetricState.getChainingKey())).to.be.true;
expect(uint8ArrayEquals(h, symmetricState.getHandshakeHash())).to.be.false;
expect(uint8ArrayEquals(decrypted, plaintext)).to.be.true;
// split
// ==========
// If at least one mixKey is executed (as above), ck is non-empty
expect(uint8ArrayEquals(symmetricState.getChainingKey(), CipherState.createEmptyKey())).to.be.false;
// When a Symmetric State's ck is non-empty, we can execute split, which creates two distinct Cipher States cs1 and cs2
// with non-empty encryption keys and nonce set to 0
const { cs1, cs2 } = symmetricState.split();
expect(uint8ArrayEquals(cs1.getKey(), CipherState.createEmptyKey())).to.be.false;
expect(uint8ArrayEquals(cs2.getKey(), CipherState.createEmptyKey())).to.be.false;
expect(cs1.getNonce().getUint64()).to.be.equals(0);
expect(cs2.getNonce().getUint64()).to.be.equals(0);
expect(uint8ArrayEquals(cs1.getKey(), cs2.getKey())).to.be.false;
});
it("Noise XX Handhshake and message encryption (extended test)", function () {
const hsPattern = NoiseHandshakePatterns.XX;
// We initialize Alice's and Bob's Handshake State
const aliceStaticKey = generateX25519KeyPair();
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, initiator: true });
const bobStaticKey = generateX25519KeyPair();
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey });
let sentTransportMessage: Uint8Array;
let aliceStep: HandshakeStepResult;
let bobStep: HandshakeStepResult;
// Here the handshake starts
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// 2nd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// At this step, Bob writes and returns a payload
bobStep = bobHS.stepHandshake({ transportMessage: sentTransportMessage });
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS.stepHandshake({ readPayloadV2: bobStep.payload2 });
expect(uint8ArrayEquals(aliceStep.transportMessage, sentTransportMessage)).to.be.true;
// 3rd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// Note that for this handshake pattern, no more message patterns are left for processing
// Another call to stepHandshake would return an empty HandshakeStepResult
// We test that extra calls to stepHandshake do not affect parties' handshake states
// and that the intermediate HandshakeStepResult are empty
const prevAliceHS = aliceHS.clone();
const prevBobHS = bobHS.clone();
const bobStep1 = bobHS.stepHandshake({ transportMessage: sentTransportMessage });
const aliceStep1 = aliceHS.stepHandshake({ readPayloadV2: bobStep1.payload2 });
const aliceStep2 = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
const bobStep2 = bobHS.stepHandshake({ readPayloadV2: aliceStep2.payload2 });
const defaultHandshakeStepResult = new HandshakeStepResult();
expect(aliceStep1.equals(defaultHandshakeStepResult)).to.be.true;
expect(aliceStep2.equals(defaultHandshakeStepResult)).to.be.true;
expect(bobStep1.equals(defaultHandshakeStepResult)).to.be.true;
expect(bobStep2.equals(defaultHandshakeStepResult)).to.be.true;
expect(aliceHS.equals(prevAliceHS)).to.be.true;
expect(bobHS.equals(prevBobHS)).to.be.true;
// After Handshake
// ==========
// We finalize the handshake to retrieve the Inbound/Outbound symmetric states
const aliceHSResult = aliceHS.finalizeHandshake();
const bobHSResult = bobHS.finalizeHandshake();
const defaultMessageNametagBuffer = new MessageNametagBuffer();
// We test read/write of random messages exchanged between Alice and Bob
for (let i = 0; i < 10; i++) {
// Alice writes to Bob
let message = randomBytes(32);
let payload2 = aliceHSResult.writeMessage(message, defaultMessageNametagBuffer);
let readMessage = bobHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
// Bob writes to Alice
message = randomBytes(32);
payload2 = bobHSResult.writeMessage(message, defaultMessageNametagBuffer);
readMessage = aliceHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
}
});
it("Noise XXpsk0 Handhshake and message encryption (short test)", function () {
const hsPattern = NoiseHandshakePatterns.XXpsk0;
// We generate a random psk
const psk = randomBytes(32, rng);
// We initialize Alice's and Bob's Handshake State
const aliceStaticKey = generateX25519KeyPair();
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, psk, initiator: true });
const bobStaticKey = generateX25519KeyPair();
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey, psk });
let sentTransportMessage: Uint8Array;
let aliceStep: HandshakeStepResult;
let bobStep: HandshakeStepResult;
// Here the handshake starts
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// 2nd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// At this step, Bob writes and returns a payload
bobStep = bobHS.stepHandshake({ transportMessage: sentTransportMessage });
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS.stepHandshake({ readPayloadV2: bobStep.payload2 });
expect(uint8ArrayEquals(aliceStep.transportMessage, sentTransportMessage)).to.be.true;
// 3rd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// Note that for this handshake pattern, no more message patterns are left for processing
// After Handshake
// ==========
// We finalize the handshake to retrieve the Inbound/Outbound symmetric states
const aliceHSResult = aliceHS.finalizeHandshake();
const bobHSResult = bobHS.finalizeHandshake();
const defaultMessageNametagBuffer = new MessageNametagBuffer();
// We test read/write of random messages exchanged between Alice and Bob
for (let i = 0; i < 10; i++) {
// Alice writes to Bob
let message = randomBytes(32);
let payload2 = aliceHSResult.writeMessage(message, defaultMessageNametagBuffer);
let readMessage = bobHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
// Bob writes to Alice
message = randomBytes(32);
payload2 = bobHSResult.writeMessage(message, defaultMessageNametagBuffer);
readMessage = aliceHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
}
});
it("Noise K1K1 Handhshake and message encryption (short test)", function () {
const hsPattern = NoiseHandshakePatterns.K1K1;
// We initialize Alice's and Bob's Handshake State
const aliceStaticKey = generateX25519KeyPair();
const bobStaticKey = generateX25519KeyPair();
// This handshake has the following pre-message pattern:
// -> s
// <- s
// ...
// So we define accordingly the sequence of the pre-message public keys
const preMessagePKs = [NoisePublicKey.to(aliceStaticKey.publicKey), NoisePublicKey.to(bobStaticKey.publicKey)];
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, preMessagePKs, initiator: true });
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey, preMessagePKs });
let sentTransportMessage: Uint8Array;
let aliceStep: HandshakeStepResult;
let bobStep: HandshakeStepResult;
// Here the handshake starts
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// 2nd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// At this step, Bob writes and returns a payload
bobStep = bobHS.stepHandshake({ transportMessage: sentTransportMessage });
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS.stepHandshake({ readPayloadV2: bobStep.payload2 });
expect(uint8ArrayEquals(aliceStep.transportMessage, sentTransportMessage)).to.be.true;
// 3rd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// Note that for this handshake pattern, no more message patterns are left for processing
// After Handshake
// ==========
// We finalize the handshake to retrieve the Inbound/Outbound symmetric states
const aliceHSResult = aliceHS.finalizeHandshake();
const bobHSResult = bobHS.finalizeHandshake();
const defaultMessageNametagBuffer = new MessageNametagBuffer();
// We test read/write of random messages exchanged between Alice and Bob
for (let i = 0; i < 10; i++) {
// Alice writes to Bob
let message = randomBytes(32);
let payload2 = aliceHSResult.writeMessage(message, defaultMessageNametagBuffer);
let readMessage = bobHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
// Bob writes to Alice
message = randomBytes(32);
payload2 = bobHSResult.writeMessage(message, defaultMessageNametagBuffer);
readMessage = aliceHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
}
});
it("Noise XK1 Handhshake and message encryption (short test)", function () {
const hsPattern = NoiseHandshakePatterns.XK1;
// We initialize Alice's and Bob's Handshake State
const aliceStaticKey = generateX25519KeyPair();
const bobStaticKey = generateX25519KeyPair();
// This handshake has the following pre-message pattern:
// <- s
// ...
// So we define accordingly the sequence of the pre-message public keys
const preMessagePKs = [NoisePublicKey.to(bobStaticKey.publicKey)];
const aliceHS = new Handshake({ hsPattern, staticKey: aliceStaticKey, preMessagePKs, initiator: true });
const bobHS = new Handshake({ hsPattern, staticKey: bobStaticKey, preMessagePKs });
let sentTransportMessage: Uint8Array;
let aliceStep: HandshakeStepResult;
let bobStep: HandshakeStepResult;
// Here the handshake starts
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// 2nd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// At this step, Bob writes and returns a payload
bobStep = bobHS.stepHandshake({ transportMessage: sentTransportMessage });
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS.stepHandshake({ readPayloadV2: bobStep.payload2 });
expect(uint8ArrayEquals(aliceStep.transportMessage, sentTransportMessage)).to.be.true;
// 3rd step
// ==========
// We generate a random transport message
sentTransportMessage = randomBytes(32, rng);
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage });
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: aliceStep.payload2 });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage)).to.be.true;
// Note that for this handshake pattern, no more message patterns are left for processing
// After Handshake
// ==========
// We finalize the handshake to retrieve the Inbound/Outbound symmetric states
const aliceHSResult = aliceHS.finalizeHandshake();
const bobHSResult = bobHS.finalizeHandshake();
const defaultMessageNametagBuffer = new MessageNametagBuffer();
// We test read/write of random messages exchanged between Alice and Bob
for (let i = 0; i < 10; i++) {
// Alice writes to Bob
let message = randomBytes(32);
let payload2 = aliceHSResult.writeMessage(message, defaultMessageNametagBuffer);
let readMessage = bobHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
// Bob writes to Alice
message = randomBytes(32);
payload2 = bobHSResult.writeMessage(message, defaultMessageNametagBuffer);
readMessage = aliceHSResult.readMessage(payload2, defaultMessageNametagBuffer);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
}
});
});

View File

@ -3,12 +3,7 @@ import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import type { bytes32 } from "./@types/basic.js";
import {
chaCha20Poly1305Decrypt,
chaCha20Poly1305Encrypt,
getHKDF,
hashSHA256,
} from "./crypto.js";
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, getHKDF, hashSHA256 } from "./crypto.js";
import { Nonce } from "./nonce.js";
import { HandshakePattern } from "./patterns.js";
@ -48,9 +43,17 @@ export class CipherState {
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce;
constructor(k: bytes32 = CipherState.createEmptyKey()) {
constructor(k: bytes32 = CipherState.createEmptyKey(), n = new Nonce()) {
this.k = k;
this.n = new Nonce();
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();
}
// Checks if a Cipher State has an encryption key set
@ -76,13 +79,7 @@ export class CipherState {
if (this.hasKey()) {
// If an encryption key is set in the Cipher state, we proceed with encryption
ciphertext = chaCha20Poly1305Encrypt(
plaintext,
this.n.getBytes(),
ad,
this.k
);
ciphertext = chaCha20Poly1305Encrypt(plaintext, this.n.getBytes(), ad, this.k);
this.n.increment();
this.n.assertValue();
@ -90,9 +87,7 @@ export class CipherState {
} else {
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
ciphertext = plaintext;
console.debug(
"encryptWithAd called with no encryption key set. Returning plaintext."
);
console.debug("encryptWithAd called with no encryption key set. Returning plaintext.");
}
return ciphertext;
@ -104,14 +99,9 @@ export class CipherState {
this.n.assertValue();
if (this.hasKey()) {
const plaintext = chaCha20Poly1305Decrypt(
ciphertext,
this.n.getBytes(),
ad,
this.k
);
const plaintext = chaCha20Poly1305Decrypt(ciphertext, this.n.getBytes(), ad, this.k);
if (!plaintext) {
throw "decryptWithAd failed";
throw new Error("decryptWithAd failed");
}
this.n.increment();
@ -121,9 +111,7 @@ export class CipherState {
} else {
// Otherwise we return the input ciphertext according to specification
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object
console.debug(
"decryptWithAd called with no encryption key set. Returning ciphertext."
);
console.debug("decryptWithAd called with no encryption key set. Returning ciphertext.");
return ciphertext;
}
}
@ -170,11 +158,30 @@ export class SymmetricState {
cs: CipherState;
ck: bytes32; // chaining key
h: bytes32; // handshake hash
hsPattern: HandshakePattern;
constructor(hsPattern: HandshakePattern) {
this.h = hashProtocol(hsPattern.name);
this.ck = this.h;
this.cs = new CipherState();
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;
}
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
@ -192,9 +199,7 @@ export class SymmetricState {
// 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
this.h = hashSHA256(
uint8ArrayConcat([this.h, data], this.h.length + data.length)
);
this.h = hashSHA256(uint8ArrayConcat([this.h, data]));
console.trace("mixHash", this.h);
}
@ -215,10 +220,7 @@ export class SymmetricState {
// 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)
encryptAndHash(
plaintext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
// The additional data
const ad = uint8ArrayConcat([this.h, extraAd]);
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintext
@ -231,10 +233,7 @@ export class SymmetricState {
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines decryptWithAd and mixHash
decryptAndHash(
ciphertext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
// 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

View File

@ -9,8 +9,7 @@ export const MIN_NONCE = 0;
// this MAX_NONCE is still a large number of messages, so the practical effect of this is negligible.
export const MAX_NONCE = 0xffffffff;
const ERR_MAX_NONCE =
"Cipherstate has reached maximum n, a new handshake must be performed";
const ERR_MAX_NONCE = "Cipherstate has reached maximum n, a new handshake must be performed";
/**
* The nonce is an uint that's increased over time.
@ -24,11 +23,7 @@ export class Nonce {
constructor(n = MIN_NONCE) {
this.n = n;
this.bytes = new Uint8Array(12);
this.view = new DataView(
this.bytes.buffer,
this.bytes.byteOffset,
this.bytes.byteLength
);
this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
this.view.setUint32(4, n, true);
}
@ -46,6 +41,14 @@ export class Nonce {
return this.n;
}
clone(): Nonce {
return new Nonce(this.n);
}
equals(b: Nonce): boolean {
return b.n == this.n;
}
assertValue(): void {
if (this.n > MAX_NONCE) {
throw new Error(ERR_MAX_NONCE);

View File

@ -1,4 +1,5 @@
// The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
export enum NoiseTokens {
e = "e",
@ -47,6 +48,14 @@ export class MessagePattern {
this.direction = direction;
this.tokens = tokens;
}
equals(b: MessagePattern): boolean {
return (
this.direction == b.direction &&
this.tokens.length === b.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index])
);
}
}
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
@ -55,15 +64,25 @@ export class HandshakePattern {
preMessagePatterns: Array<PreMessagePattern>;
messagePatterns: Array<MessagePattern>;
constructor(
name: string,
preMessagePatterns: Array<PreMessagePattern>,
messagePatterns: Array<MessagePattern>
) {
constructor(name: string, preMessagePatterns: Array<PreMessagePattern>, messagePatterns: Array<MessagePattern>) {
this.name = name;
this.preMessagePatterns = preMessagePatterns;
this.messagePatterns = messagePatterns;
}
equals(b: HandshakePattern): boolean {
if (this.preMessagePatterns.length != b.preMessagePatterns.length) return false;
for (let i = 0; i < this.preMessagePatterns.length; i++) {
if (!this.preMessagePatterns[i].equals(b.preMessagePatterns[i])) return false;
}
if (this.messagePatterns.length != b.messagePatterns.length) return false;
for (let i = 0; i < this.messagePatterns.length; i++) {
if (!this.messagePatterns[i].equals(b.messagePatterns[i])) return false;
}
return this.name == b.name;
}
}
// Constants (supported protocols)
@ -79,11 +98,7 @@ export const NoiseHandshakePatterns = {
],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.se]),
]
),
@ -92,53 +107,27 @@ export const NoiseHandshakePatterns = {
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
XX: new HandshakePattern(
"Noise_XX_25519_ChaChaPoly_SHA256",
EmptyPreMessage,
[
XX: new HandshakePattern("Noise_XX_25519_ChaChaPoly_SHA256", EmptyPreMessage, [
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.s,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
XXpsk0: new HandshakePattern(
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
EmptyPreMessage,
[
]),
XXpsk0: new HandshakePattern("Noise_XXpsk0_25519_ChaChaPoly_SHA256", EmptyPreMessage, [
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.s,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
]),
WakuPairing: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e, NoiseTokens.ee]),
new MessagePattern(MessageDirection.l, [NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [
NoiseTokens.s,
NoiseTokens.se,
NoiseTokens.ss,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se, NoiseTokens.ss]),
]
),
};

View File

@ -19,16 +19,8 @@ export function toMessageNametag(input: Uint8Array): MessageNametag {
// Adapted from https://github.com/feross/buffer
function checkInt(
buf: Uint8Array,
value: number,
offset: number,
ext: number,
max: number,
min: number
): void {
if (value > max || value < min)
throw new RangeError('"value" argument is out of bounds');
function checkInt(buf: Uint8Array, value: number, offset: number, ext: number, max: number, min: number): void {
if (value > max || value < min) throw new RangeError('"value" argument is out of bounds');
if (offset + ext > buf.length) throw new RangeError("Index out of range");
}
@ -58,9 +50,7 @@ const writeUIntLE = function writeUIntLE(
};
export class MessageNametagBuffer {
buffer: Array<MessageNametag> = new Array<MessageNametag>(
MessageNametagBufferSize
);
buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
counter = 0;
secret?: Uint8Array;
@ -72,12 +62,7 @@ export class MessageNametagBuffer {
if (this.secret) {
for (let i = 0; i < this.buffer.length; i++) {
const counterBytesLE = writeUIntLE(
new Uint8Array(8),
this.counter,
0,
8
);
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[i] = toMessageNametag(d);
this.counter++;
@ -97,9 +82,7 @@ export class MessageNametagBuffer {
// Checks if the input messageNametag is contained in the input MessageNametagBuffer
checkNametag(messageNametag: MessageNametag): boolean {
const index = this.buffer.findIndex((x) =>
uint8ArrayEquals(x, messageNametag)
);
const index = this.buffer.findIndex((x) => uint8ArrayEquals(x, messageNametag));
if (index == -1) {
console.error("Message nametag not found in buffer");
@ -134,12 +117,7 @@ export class MessageNametagBuffer {
}
for (let i = 0; i < n; i++) {
const counterBytesLE = writeUIntLE(
new Uint8Array(8),
this.counter,
0,
8
);
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[this.buffer.length - n + i] = toMessageNametag(d);
@ -159,18 +137,46 @@ export class PayloadV2 {
transportMessage: Uint8Array;
constructor(
messageNametag?: MessageNametag,
protocolId?: number,
handshakeMessage?: Array<NoisePublicKey>,
transportMessage?: Uint8Array
messageNametag: MessageNametag = new Uint8Array(MessageNametagLength),
protocolId = 0,
handshakeMessage: Array<NoisePublicKey> = [],
transportMessage: Uint8Array = new Uint8Array()
) {
this.messageNametag = messageNametag
? messageNametag
: new Uint8Array(MessageNametagLength);
this.protocolId = protocolId ? protocolId : 0;
this.handshakeMessage = handshakeMessage ? handshakeMessage : [];
this.transportMessage = transportMessage
? transportMessage
: new Uint8Array();
this.messageNametag = messageNametag;
this.protocolId = protocolId;
this.handshakeMessage = handshakeMessage;
this.transportMessage = transportMessage;
}
clone(): PayloadV2 {
const r = new PayloadV2();
r.protocolId = this.protocolId;
r.transportMessage = new Uint8Array(this.transportMessage);
r.messageNametag = new Uint8Array(this.messageNametag);
for (let i = 0; i < this.handshakeMessage.length; i++) {
r.handshakeMessage.push(this.handshakeMessage[i].clone());
}
return r;
}
equals(b: PayloadV2): boolean {
let pkEquals = true;
if (this.handshakeMessage.length != b.handshakeMessage.length) {
pkEquals = false;
}
for (let i = 0; i < this.handshakeMessage.length; i++) {
if (!this.handshakeMessage[i].equals(b.handshakeMessage[i])) {
pkEquals = false;
break;
}
}
return (
uint8ArrayEquals(this.messageNametag, b.messageNametag) &&
this.protocolId == b.protocolId &&
uint8ArrayEquals(this.transportMessage, b.transportMessage) &&
pkEquals
);
}
}

View File

@ -16,6 +16,10 @@ export class NoisePublicKey {
this.pk = pk;
}
clone(): NoisePublicKey {
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
}
// Checks equality between two Noise public keys
equals(k2: NoisePublicKey): boolean {
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk);