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", "version": "0.1",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json", "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en", "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": [], "flagWords": [],
"ignorePaths": [ "ignorePaths": [
"package.json", "package.json",

View File

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

View File

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

3
.prettierrc Normal file
View File

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

View File

@ -1,7 +1,7 @@
module.exports = [ module.exports = [
{ {
name: "RLN core", name: "JS-Noice core",
path: "bundle/index.js", 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 ResolveTypeScriptPlugin = require("resolve-typescript-plugin");
const output = { const output = {
path: path: path.join(os.tmpdir(), "_karma_webpack_") + Math.floor(Math.random() * 1000000),
path.join(os.tmpdir(), "_karma_webpack_") +
Math.floor(Math.random() * 1000000),
}; };
module.exports = function (config) { module.exports = function (config) {

View File

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

View File

@ -1,4 +1,5 @@
import * as pkcs7 from "pkcs7-padding"; import * as pkcs7 from "pkcs7-padding";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic"; import { bytes32 } from "./@types/basic";
import { KeyPair } from "./@types/keypair"; import { KeyPair } from "./@types/keypair";
@ -17,6 +18,17 @@ import { NoisePublicKey } from "./publickey";
export class HandshakeStepResult { export class HandshakeStepResult {
payload2: PayloadV2 = new PayloadV2(); payload2: PayloadV2 = new PayloadV2();
transportMessage: Uint8Array = new Uint8Array(); 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 // When a handshake is complete, the HandshakeResult will contain the two
@ -31,20 +43,95 @@ export class HandshakeResult {
nametagsOutbound: MessageNametagBuffer = new MessageNametagBuffer(); nametagsOutbound: MessageNametagBuffer = new MessageNametagBuffer();
rs: bytes32 = new Uint8Array(); rs: bytes32 = new Uint8Array();
h: 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 { export class Handshake {
hs: HandshakeState; hs: HandshakeState;
constructor( constructor({
hasPattern: HandshakePattern, hsPattern,
ephemeralKey: KeyPair, ephemeralKey,
staticKey?: KeyPair, staticKey,
prologue: Uint8Array = new Uint8Array(), prologue = new Uint8Array(),
psk: Uint8Array = new Uint8Array(), psk = new Uint8Array(),
preMessagePKs: Array<NoisePublicKey> = [], preMessagePKs = [],
initiator = false initiator = false,
) { }: HandshakeParameters) {
this.hs = new HandshakeState(hasPattern, psk); this.hs = new HandshakeState(hsPattern, psk);
this.hs.ss.mixHash(prologue); this.hs.ss.mixHash(prologue);
this.hs.e = ephemeralKey; this.hs.e = ephemeralKey;
this.hs.s = staticKey; this.hs.s = staticKey;
@ -56,34 +143,40 @@ export class Handshake {
this.hs.processPreMessagePatternTokens(preMessagePKs); 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 // Advances 1 step in handshake
// Each user in a handshake alternates writing and reading of handshake messages. // 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 // 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) // 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( stepHandshake({
readPayloadV2: PayloadV2 = new PayloadV2(), readPayloadV2 = new PayloadV2(),
transportMessage: Uint8Array = new Uint8Array(), transportMessage = new Uint8Array(),
messageNametag: Uint8Array = new Uint8Array() messageNametag = new Uint8Array(),
): HandshakeStepResult { }: StepHandshakeParameters): HandshakeStepResult {
const hsStepResult = new HandshakeStepResult(); const hsStepResult = new HandshakeStepResult();
// If there are no more message patterns left for processing // If there are no more message patterns left for processing
// we return an empty HandshakeStepResult // we return an empty HandshakeStepResult
if ( if (this.hs.msgPatternIdx > this.hs.handshakePattern.messagePatterns.length - 1) {
this.hs.msgPatternIdx > console.debug("stepHandshake called more times than the number of message patterns present in handshake");
this.hs.handshakePattern.messagePatterns.length - 1
) {
console.debug(
"stepHandshake called more times than the number of message patterns present in handshake"
);
return hsStepResult; return hsStepResult;
} }
// We process the next handshake message pattern // We process the next handshake message pattern
// We get if the user is reading or writing the input handshake message // We get if the user is reading or writing the input handshake message
const direction = const direction = this.hs.handshakePattern.messagePatterns[this.hs.msgPatternIdx].direction;
this.hs.handshakePattern.messagePatterns[this.hs.msgPatternIdx].direction;
const { reading, writing } = this.hs.getReadingWritingState(direction); const { reading, writing } = this.hs.getReadingWritingState(direction);
// If we write an answer at this handshake step // If we write an answer at this handshake step
@ -91,29 +184,26 @@ export class Handshake {
// We initialize a payload v2 and we set proper protocol ID (if supported) // We initialize a payload v2 and we set proper protocol ID (if supported)
try { try {
hsStepResult.payload2.protocolId = hsStepResult.payload2.protocolId =
PayloadV2ProtocolIDs[ PayloadV2ProtocolIDs[this.hs.handshakePattern.name as keyof typeof PayloadV2ProtocolIDs];
this.hs.handshakePattern.name as keyof typeof PayloadV2ProtocolIDs
];
} catch (err) { } catch (err) {
throw "Handshake Pattern not supported"; throw new Error("handshake pattern not supported");
} }
// We set the messageNametag and the handshake and transport messages // We set the messageNametag and the handshake and transport messages
hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag); hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag);
hsStepResult.payload2.handshakeMessage = hsStepResult.payload2.handshakeMessage = this.hs.processMessagePatternTokens();
this.hs.processMessagePatternTokens();
// We write the payload by passing the messageNametag as extra additional data // We write the payload by passing the messageNametag as extra additional data
hsStepResult.payload2.transportMessage = hsStepResult.payload2.transportMessage = this.hs.processMessagePatternPayload(
this.hs.processMessagePatternPayload( transportMessage,
transportMessage, hsStepResult.payload2.messageNametag
hsStepResult.payload2.messageNametag );
);
// If we read an answer during this handshake step // If we read an answer during this handshake step
} else if (reading) { } else if (reading) {
// If the read message nametag doesn't match the expected input one we raise an error // If the read message nametag doesn't match the expected input one we raise an error
if (readPayloadV2.messageNametag != toMessageNametag(messageNametag)) { if (!uint8ArrayEquals(readPayloadV2.messageNametag, toMessageNametag(messageNametag))) {
throw "The message nametag of the read message doesn't match the expected one"; 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 // We process the read public keys and (eventually decrypt) the read transport message
@ -128,7 +218,7 @@ export class Handshake {
readPayloadV2.messageNametag readPayloadV2.messageNametag
); );
} else { } 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 // We increase the handshake state message pattern index to progress to next step
@ -177,84 +267,4 @@ export class Handshake {
return hsResult; 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 * as pkcs7 from "pkcs7-padding";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic.js"; import { bytes32 } from "./@types/basic.js";
import type { KeyPair } from "./@types/keypair.js"; import type { KeyPair } from "./@types/keypair.js";
import { import { Curve25519KeySize, dh, generateX25519KeyPair, getHKDF, intoCurve25519Key } from "./crypto.js";
Curve25519KeySize,
dh,
generateX25519KeyPair,
getHKDF,
intoCurve25519Key,
} from "./crypto.js";
import { SymmetricState } from "./noise.js"; import { SymmetricState } from "./noise.js";
import { import { EmptyPreMessage, HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns";
EmptyPreMessage,
HandshakePattern,
MessageDirection,
NoiseTokens,
PreMessagePattern,
} from "./patterns";
import { NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
// The padding blocksize of a transport message // The padding blocksize of a transport message
@ -54,6 +43,58 @@ export class HandshakeState {
this.msgPatternIdx = 0; 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 } { genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } {
const [nms1, nms2] = getHKDF(this.ss.h, new Uint8Array()); const [nms1, nms2] = getHKDF(this.ss.h, new Uint8Array());
return { nms1, nms2 }; return { nms1, nms2 };
@ -119,11 +160,9 @@ export class HandshakeState {
// Handshake messages processing procedures // Handshake messages processing procedures
// Processes pre-message patterns // Processes pre-message patterns
processPreMessagePatternTokens( processPreMessagePatternTokens(inPreMessagePKs: Array<NoisePublicKey> = []): void {
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 // 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 // Here we store currently processed pre message public key
let currPK: NoisePublicKey; 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 not empty, we check that pre-message is valid according to Noise specifications
if (!this.isValid(this.handshakePattern.preMessagePatterns)) { 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 // We iterate over each pattern contained in the pre-message
@ -156,7 +195,7 @@ export class HandshakeState {
if (preMessagePKs.length > 0) { if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0]; currPK = preMessagePKs[0];
} else { } 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 // If user is reading the "e" token
@ -169,7 +208,7 @@ export class HandshakeState {
this.re = intoCurve25519Key(currPK.pk); this.re = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.re); this.ss.mixHash(this.re);
} else { } 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 // If user is writing the "e" token
} else if (writing) { } else if (writing) {
@ -177,10 +216,10 @@ export class HandshakeState {
// When writing, the user is sending a public key, // 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). // 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); this.ss.mixHash(this.e.publicKey);
} else { } 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) { if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0]; currPK = preMessagePKs[0];
} else { } 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 // If user is reading the "s" token
@ -213,7 +252,7 @@ export class HandshakeState {
this.rs = intoCurve25519Key(currPK.pk); this.rs = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.rs); this.ss.mixHash(this.rs);
} else { } 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 // 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, // 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). // 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); this.ss.mixHash(this.s.publicKey);
} else { } 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(); preMessagePKs.shift();
break; break;
default: 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 // 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) // An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
processMessagePatternPayload( processMessagePatternPayload(transportMessage: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
transportMessage: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
let payload: Uint8Array; let payload: Uint8Array;
// We retrieve current message pattern (direction + tokens) to process // We retrieve current message pattern (direction + tokens) to process
const direction = const direction = this.handshakePattern.messagePatterns[this.msgPatternIdx].direction;
this.handshakePattern.messagePatterns[this.msgPatternIdx].direction;
// We get if the user is reading or writing the input handshake message // We get if the user is reading or writing the input handshake message
const { reading, writing } = this.getReadingWritingState(direction); const { reading, writing } = this.getReadingWritingState(direction);
@ -265,23 +300,20 @@ export class HandshakeState {
// We decrypt the transportMessage, if any // We decrypt the transportMessage, if any
if (reading) { if (reading) {
payload = this.ss.decryptAndHash(transportMessage, extraAd); payload = this.ss.decryptAndHash(transportMessage, extraAd);
payload = pkcs7.pad(payload, NoisePaddingBlockSize); payload = pkcs7.unpad(payload);
} else if (writing) { } else if (writing) {
payload = pkcs7.unpad(transportMessage); payload = pkcs7.pad(transportMessage, NoisePaddingBlockSize);
payload = this.ss.encryptAndHash(payload, extraAd); payload = this.ss.encryptAndHash(payload, extraAd);
} else { } else {
throw "undefined state"; throw new Error("undefined state");
} }
return payload; return payload;
} }
// We process an input handshake message according to current handshake state and we return the next handshake step's handshake message // We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
processMessagePatternTokens( processMessagePatternTokens(inputHandshakeMessage: Array<NoisePublicKey> = []): Array<NoisePublicKey> {
inputHandshakeMessage: Array<NoisePublicKey> = []
): Array<NoisePublicKey> {
// We retrieve current message pattern (direction + tokens) to process // We retrieve current message pattern (direction + tokens) to process
const messagePattern = const messagePattern = this.handshakePattern.messagePatterns[this.msgPatternIdx];
this.handshakePattern.messagePatterns[this.msgPatternIdx];
const direction = messagePattern.direction; const direction = messagePattern.direction;
const tokens = messagePattern.tokens; const tokens = messagePattern.tokens;
@ -311,7 +343,7 @@ export class HandshakeState {
if (inHandshakeMessage.length > 0) { if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0]; currPK = inHandshakeMessage[0];
} else { } 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 // 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). // Decrypts re, sets re and calls MixHash(re.public_key).
this.re = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk)); this.re = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else { } 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 // Noise specification: section 9.2
@ -374,7 +406,7 @@ export class HandshakeState {
if (inHandshakeMessage.length > 0) { if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0]; currPK = inHandshakeMessage[0];
} else { } 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 // 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). // Decrypts rs, sets rs and calls MixHash(rs.public_key).
this.rs = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk)); this.rs = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else { } 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 // 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 the local static key is not set (the handshake state was not properly initialized), we raise an error
if (!this.s) { 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 // 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 local and/or remote ephemeral keys are not set, we raise an error
if (!this.e || !this.re) { 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)). // 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 both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
if (this.initiator) { if (this.initiator) {
if (!this.e || !this.rs) { 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)); this.ss.mixKey(dh(this.e.privateKey, this.rs));
} else { } else {
if (!this.re || !this.s) { 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)); 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 both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
if (this.initiator) { if (this.initiator) {
if (!this.s || !this.re) { 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)); this.ss.mixKey(dh(this.s.privateKey, this.re));
} else { } else {
if (!this.rs || !this.e) { 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)); 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 local and/or remote static keys are not set, we raise an error
if (!this.s || !this.rs) { 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)). // Calls MixKey(DH(s, rs)).

View File

@ -4,8 +4,12 @@ import { expect } from "chai";
import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto"; 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 { 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 { function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
const randomCipherState = new CipherState(); const randomCipherState = new CipherState();
@ -14,7 +18,13 @@ function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
return randomCipherState; return randomCipherState;
} }
function c(input: Uint8Array): Uint8Array {
return new Uint8Array(input);
}
describe("js-noise", () => { describe("js-noise", () => {
const rng = new HMACDRBG();
it("Noise State Machine: Diffie-Hellman operation", function () { it("Noise State Machine: Diffie-Hellman operation", function () {
const aliceKey = generateX25519KeyPair(); const aliceKey = generateX25519KeyPair();
const bobKey = generateX25519KeyPair(); const bobKey = generateX25519KeyPair();
@ -28,8 +38,6 @@ describe("js-noise", () => {
}); });
it("Noise State Machine: Cipher State primitives", function () { it("Noise State Machine: Cipher State primitives", function () {
const rng = new HMACDRBG();
// We generate a random Cipher State, associated data ad and plaintext // We generate a random Cipher State, associated data ad and plaintext
let cipherState = randomCipherState(rng); let cipherState = randomCipherState(rng);
let nonceValue = Math.floor(Math.random() * MAX_NONCE); let nonceValue = Math.floor(Math.random() * MAX_NONCE);
@ -104,12 +112,7 @@ describe("js-noise", () => {
plaintext = randomBytes(128, rng); plaintext = randomBytes(128, rng);
// We perform encryption using the Cipher State key, NonceMax and ad // We perform encryption using the Cipher State key, NonceMax and ad
ciphertext = chaCha20Poly1305Encrypt( ciphertext = chaCha20Poly1305Encrypt(plaintext, cipherState.getNonce().getBytes(), ad, cipherState.getKey());
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 // 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 // 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); 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 { equals as uint8ArrayEquals } from "uint8arrays/equals";
import type { bytes32 } from "./@types/basic.js"; import type { bytes32 } from "./@types/basic.js";
import { import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, getHKDF, hashSHA256 } from "./crypto.js";
chaCha20Poly1305Decrypt,
chaCha20Poly1305Encrypt,
getHKDF,
hashSHA256,
} from "./crypto.js";
import { Nonce } from "./nonce.js"; import { Nonce } from "./nonce.js";
import { HandshakePattern } from "./patterns.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. // The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce; n: Nonce;
constructor(k: bytes32 = CipherState.createEmptyKey()) { constructor(k: bytes32 = CipherState.createEmptyKey(), n = new Nonce()) {
this.k = k; 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 // Checks if a Cipher State has an encryption key set
@ -76,13 +79,7 @@ export class CipherState {
if (this.hasKey()) { if (this.hasKey()) {
// If an encryption key is set in the Cipher state, we proceed with encryption // 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.increment();
this.n.assertValue(); this.n.assertValue();
@ -90,9 +87,7 @@ export class CipherState {
} else { } else {
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object // Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
ciphertext = plaintext; ciphertext = plaintext;
console.debug( console.debug("encryptWithAd called with no encryption key set. Returning plaintext.");
"encryptWithAd called with no encryption key set. Returning plaintext."
);
} }
return ciphertext; return ciphertext;
@ -104,14 +99,9 @@ export class CipherState {
this.n.assertValue(); this.n.assertValue();
if (this.hasKey()) { if (this.hasKey()) {
const plaintext = chaCha20Poly1305Decrypt( const plaintext = chaCha20Poly1305Decrypt(ciphertext, this.n.getBytes(), ad, this.k);
ciphertext,
this.n.getBytes(),
ad,
this.k
);
if (!plaintext) { if (!plaintext) {
throw "decryptWithAd failed"; throw new Error("decryptWithAd failed");
} }
this.n.increment(); this.n.increment();
@ -121,9 +111,7 @@ export class CipherState {
} else { } else {
// Otherwise we return the input ciphertext according to specification // Otherwise we return the input ciphertext according to specification
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object // http://www.noiseprotocol.org/noise.html#the-cipherstate-object
console.debug( console.debug("decryptWithAd called with no encryption key set. Returning ciphertext.");
"decryptWithAd called with no encryption key set. Returning ciphertext."
);
return ciphertext; return ciphertext;
} }
} }
@ -170,11 +158,30 @@ export class SymmetricState {
cs: CipherState; cs: CipherState;
ck: bytes32; // chaining key ck: bytes32; // chaining key
h: bytes32; // handshake hash h: bytes32; // handshake hash
hsPattern: HandshakePattern;
constructor(hsPattern: HandshakePattern) { constructor(hsPattern: HandshakePattern) {
this.h = hashProtocol(hsPattern.name); this.h = hashProtocol(hsPattern.name);
this.ck = this.h; this.ck = this.h;
this.cs = new CipherState(); 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 // 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 // Hashes data into a Symmetric State's handshake hash value h
mixHash(data: Uint8Array): void { 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 // We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
this.h = hashSHA256( this.h = hashSHA256(uint8ArrayConcat([this.h, data]));
uint8ArrayConcat([this.h, data], this.h.length + data.length)
);
console.trace("mixHash", this.h); 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 // EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines encryptWithAd and mixHash // 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) // 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( encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
plaintext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
// The additional data // The additional data
const ad = uint8ArrayConcat([this.h, extraAd]); 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 // 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 // DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines decryptWithAd and mixHash // Combines decryptWithAd and mixHash
decryptAndHash( decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
ciphertext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
// The additional data // The additional data
const ad = uint8ArrayConcat([this.h, extraAd]); 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 // 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. // this MAX_NONCE is still a large number of messages, so the practical effect of this is negligible.
export const MAX_NONCE = 0xffffffff; export const MAX_NONCE = 0xffffffff;
const ERR_MAX_NONCE = const ERR_MAX_NONCE = "Cipherstate has reached maximum n, a new handshake must be performed";
"Cipherstate has reached maximum n, a new handshake must be performed";
/** /**
* The nonce is an uint that's increased over time. * The nonce is an uint that's increased over time.
@ -24,11 +23,7 @@ export class Nonce {
constructor(n = MIN_NONCE) { constructor(n = MIN_NONCE) {
this.n = n; this.n = n;
this.bytes = new Uint8Array(12); this.bytes = new Uint8Array(12);
this.view = new DataView( this.view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
this.bytes.buffer,
this.bytes.byteOffset,
this.bytes.byteLength
);
this.view.setUint32(4, n, true); this.view.setUint32(4, n, true);
} }
@ -46,6 +41,14 @@ export class Nonce {
return this.n; return this.n;
} }
clone(): Nonce {
return new Nonce(this.n);
}
equals(b: Nonce): boolean {
return b.n == this.n;
}
assertValue(): void { assertValue(): void {
if (this.n > MAX_NONCE) { if (this.n > MAX_NONCE) {
throw new Error(ERR_MAX_NONCE); throw new Error(ERR_MAX_NONCE);

View File

@ -1,4 +1,5 @@
// The Noise tokens appearing in Noise (pre)message patterns // The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics // as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
export enum NoiseTokens { export enum NoiseTokens {
e = "e", e = "e",
@ -47,6 +48,14 @@ export class MessagePattern {
this.direction = direction; this.direction = direction;
this.tokens = tokens; 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 // 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>; preMessagePatterns: Array<PreMessagePattern>;
messagePatterns: Array<MessagePattern>; messagePatterns: Array<MessagePattern>;
constructor( constructor(name: string, preMessagePatterns: Array<PreMessagePattern>, messagePatterns: Array<MessagePattern>) {
name: string,
preMessagePatterns: Array<PreMessagePattern>,
messagePatterns: Array<MessagePattern>
) {
this.name = name; this.name = name;
this.preMessagePatterns = preMessagePatterns; this.preMessagePatterns = preMessagePatterns;
this.messagePatterns = messagePatterns; 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) // Constants (supported protocols)
@ -79,11 +98,7 @@ export const NoiseHandshakePatterns = {
], ],
[ [
new MessagePattern(MessageDirection.r, [NoiseTokens.e]), new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [ new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.es]),
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.se]), new MessagePattern(MessageDirection.r, [NoiseTokens.se]),
] ]
), ),
@ -92,53 +107,27 @@ export const NoiseHandshakePatterns = {
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])], [new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])],
[ [
new MessagePattern(MessageDirection.r, [NoiseTokens.e]), new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [ new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.es]),
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
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.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
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.r, [NoiseTokens.s, NoiseTokens.se]), new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
] ]
), ),
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.r, [NoiseTokens.s, NoiseTokens.se]),
]),
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.r, [NoiseTokens.s, NoiseTokens.se]),
]),
WakuPairing: new HandshakePattern( WakuPairing: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256", "Noise_WakuPairing_25519_ChaChaPoly_SHA256",
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])], [new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
[ [
new MessagePattern(MessageDirection.r, [NoiseTokens.e, NoiseTokens.ee]), new MessagePattern(MessageDirection.r, [NoiseTokens.e, NoiseTokens.ee]),
new MessagePattern(MessageDirection.l, [NoiseTokens.s, NoiseTokens.es]), new MessagePattern(MessageDirection.l, [NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [ new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se, NoiseTokens.ss]),
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 // Adapted from https://github.com/feross/buffer
function checkInt( function checkInt(buf: Uint8Array, value: number, offset: number, ext: number, max: number, min: number): void {
buf: Uint8Array, if (value > max || value < min) throw new RangeError('"value" argument is out of bounds');
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"); if (offset + ext > buf.length) throw new RangeError("Index out of range");
} }
@ -58,9 +50,7 @@ const writeUIntLE = function writeUIntLE(
}; };
export class MessageNametagBuffer { export class MessageNametagBuffer {
buffer: Array<MessageNametag> = new Array<MessageNametag>( buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
MessageNametagBufferSize
);
counter = 0; counter = 0;
secret?: Uint8Array; secret?: Uint8Array;
@ -72,12 +62,7 @@ export class MessageNametagBuffer {
if (this.secret) { if (this.secret) {
for (let i = 0; i < this.buffer.length; i++) { for (let i = 0; i < this.buffer.length; i++) {
const counterBytesLE = writeUIntLE( const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
new Uint8Array(8),
this.counter,
0,
8
);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE])); const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[i] = toMessageNametag(d); this.buffer[i] = toMessageNametag(d);
this.counter++; this.counter++;
@ -97,9 +82,7 @@ export class MessageNametagBuffer {
// Checks if the input messageNametag is contained in the input MessageNametagBuffer // Checks if the input messageNametag is contained in the input MessageNametagBuffer
checkNametag(messageNametag: MessageNametag): boolean { checkNametag(messageNametag: MessageNametag): boolean {
const index = this.buffer.findIndex((x) => const index = this.buffer.findIndex((x) => uint8ArrayEquals(x, messageNametag));
uint8ArrayEquals(x, messageNametag)
);
if (index == -1) { if (index == -1) {
console.error("Message nametag not found in buffer"); console.error("Message nametag not found in buffer");
@ -134,12 +117,7 @@ export class MessageNametagBuffer {
} }
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const counterBytesLE = writeUIntLE( const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
new Uint8Array(8),
this.counter,
0,
8
);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE])); const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[this.buffer.length - n + i] = toMessageNametag(d); this.buffer[this.buffer.length - n + i] = toMessageNametag(d);
@ -159,18 +137,46 @@ export class PayloadV2 {
transportMessage: Uint8Array; transportMessage: Uint8Array;
constructor( constructor(
messageNametag?: MessageNametag, messageNametag: MessageNametag = new Uint8Array(MessageNametagLength),
protocolId?: number, protocolId = 0,
handshakeMessage?: Array<NoisePublicKey>, handshakeMessage: Array<NoisePublicKey> = [],
transportMessage?: Uint8Array transportMessage: Uint8Array = new Uint8Array()
) { ) {
this.messageNametag = messageNametag this.messageNametag = messageNametag;
? messageNametag this.protocolId = protocolId;
: new Uint8Array(MessageNametagLength); this.handshakeMessage = handshakeMessage;
this.protocolId = protocolId ? protocolId : 0; this.transportMessage = transportMessage;
this.handshakeMessage = handshakeMessage ? handshakeMessage : []; }
this.transportMessage = transportMessage
? transportMessage clone(): PayloadV2 {
: new Uint8Array(); 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; this.pk = pk;
} }
clone(): NoisePublicKey {
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
}
// Checks equality between two Noise public keys // Checks equality between two Noise public keys
equals(k2: NoisePublicKey): boolean { equals(k2: NoisePublicKey): boolean {
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk); return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk);