mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-02 05:33:09 +00:00
test: handshakes
This commit is contained in:
parent
11b97bafaf
commit
031c9e073b
16
.cspell.json
16
.cspell.json
@ -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",
|
||||
|
||||
@ -22,6 +22,12 @@
|
||||
"WebAssembly": true
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"printWidth": 120
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@ -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
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 120
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
module.exports = [
|
||||
{
|
||||
name: "RLN core",
|
||||
name: "JS-Noice core",
|
||||
path: "bundle/index.js",
|
||||
import: "{ RLN }",
|
||||
import: "{ Noise }",
|
||||
},
|
||||
];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
246
src/handshake.ts
246
src/handshake.ts
@ -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,29 +184,26 @@ 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(
|
||||
transportMessage,
|
||||
hsStepResult.payload2.messageNametag
|
||||
);
|
||||
hsStepResult.payload2.transportMessage = this.hs.processMessagePatternPayload(
|
||||
transportMessage,
|
||||
hsStepResult.payload2.messageNametag
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)).
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
77
src/noise.ts
77
src/noise.ts
@ -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
|
||||
|
||||
17
src/nonce.ts
17
src/nonce.ts
@ -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);
|
||||
|
||||
@ -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.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.l, [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]),
|
||||
]),
|
||||
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]),
|
||||
]
|
||||
),
|
||||
};
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user