test: waku noise sessions

This commit is contained in:
Richard Ramos 2022-11-21 18:07:21 -04:00
parent aa2490825b
commit 0fb4bb3183
No known key found for this signature in database
GPG Key ID: BD36D48BC9FFC88C
7 changed files with 5731 additions and 14 deletions

View File

@ -15,7 +15,8 @@
"HASHLEN",
"ciphertext",
"preshared",
"libp2p"
"libp2p",
"Authcode"
],
"flagWords": [],
"ignorePaths": [

5374
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
}
},
"type": "module",
"repository": "https://github.com/waku-org/js-noise",
"scripts": {
"prepare": "husky install",
"build": "run-s build:**",
@ -37,8 +38,11 @@
"engines": {
"node": ">=16"
},
"author": "",
"license": "Apache-2.0 OR MIT",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
@ -66,6 +70,7 @@
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"ignore-loader": "^0.1.2",
"js-waku": "^0.29.0-29436ea",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12",
@ -114,6 +119,7 @@
"@stablelib/random": "^1.0.2",
"@stablelib/sha256": "^1.0.1",
"@stablelib/x25519": "^1.0.1",
"js-base64": "^3.7.3",
"pkcs7-padding": "^0.1.1",
"uint8arraylist": "^2.3.2",
"uint8arrays": "^4.0.2"

100
src/codec.ts Normal file
View File

@ -0,0 +1,100 @@
import debug from "debug";
import { proto_message } from "js-waku";
import { Decoder, Encoder, Message, ProtoMessage } from "js-waku/lib/interfaces";
import { MessageV0 } from "js-waku/lib/waku_message/version_0";
import { HandshakeResult, HandshakeStepResult } from "./handshake";
import { PayloadV2 } from "./payload";
const log = debug("waku:message:noise-encoder");
const OneMillion = BigInt(1_000_000);
export const Version = 2;
export class NoiseHandshakeEncoder implements Encoder {
constructor(public contentTopic: string, private hsStepResult: HandshakeStepResult) {}
async encode(message: Message): Promise<Uint8Array | undefined> {
const protoMessage = await this.encodeProto(message);
if (!protoMessage) return;
return proto_message.WakuMessage.encode(protoMessage);
}
async encodeProto(message: Message): Promise<ProtoMessage | undefined> {
const timestamp = message.timestamp ?? new Date();
return {
payload: this.hsStepResult.payload2.serialize(),
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
};
}
}
export class MessageV2 extends MessageV0 implements Message {
get payloadV2(): PayloadV2 {
return PayloadV2.deserialize(this.payload!);
}
}
export class NoiseHandshakeDecoder implements Decoder<MessageV2> {
constructor(public contentTopic: string) {}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
const protoMessage = proto_message.WakuMessage.decode(bytes);
log("Message decoded", protoMessage);
return Promise.resolve(protoMessage);
}
async decode(proto: ProtoMessage): Promise<MessageV2 | undefined> {
// https://github.com/status-im/js-waku/issues/921
if (proto.version === undefined) {
proto.version = 0;
}
if (proto.version !== Version) {
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version);
return Promise.resolve(undefined);
}
if (!proto.payload) {
log("No payload, skipping: ", proto);
return;
}
return new MessageV2(proto);
}
}
export class NoiseSecureTransferEncoder implements Encoder {
constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
async encode(message: Message): Promise<Uint8Array | undefined> {
const protoMessage = await this.encodeProto(message);
if (!protoMessage) return;
return proto_message.WakuMessage.encode(protoMessage);
}
async encodeProto(message: Message): Promise<ProtoMessage | undefined> {
const timestamp = message.timestamp ?? new Date();
if (!message.payload) {
log("No payload to encrypt, skipping: ", message);
return;
}
const preparedPayload = this.hsResult.writeMessage(message.payload, this.hsResult.nametagsOutbound);
const payload = preparedPayload.serialize();
return {
payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
};
}
}
/*
export class NoiseSecureTransferDecoder implements Decoder<> {}
*/

View File

@ -3,6 +3,7 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic";
import { KeyPair } from "./@types/keypair";
//import { getHKDF } from "./crypto";
import { HandshakeState, NoisePaddingBlockSize } from "./handshake_state";
import { CipherState } from "./noise";
import { HandshakePattern, PayloadV2ProtocolIDs } from "./patterns";
@ -155,6 +156,15 @@ export class Handshake {
return result;
}
// Generates an 8 decimal digits authorization code using HKDF and the handshake state
genAuthcode(): string {
//var output: array[1, array[8, byte]]
// const [output0] = getHKDF(this.hs.ss.h, new Uint8Array());
// let code = cast[uint64](output[0]) mod 100_000_000
// return $code
return "TODO: implement";
}
// 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

View File

@ -1,5 +1,7 @@
// Adapted from https://github.com/feross/buffer
import { decode, encode, fromUint8Array, toUint8Array } from "js-base64";
import { bytes32 } from "./@types/basic";
function checkInt(buf: Uint8Array, value: number, offset: number, ext: number, max: number, min: number): void {
@ -60,12 +62,12 @@ export function toQr(
ephemeralKey: bytes32,
committedStaticKey: bytes32
): string {
const decoder = new TextDecoder("utf8");
let qr = window.btoa(applicationName) + ":";
qr += window.btoa(applicationVersion) + ":";
qr += window.btoa(shardId) + ":";
qr += window.btoa(decoder.decode(ephemeralKey)) + ":";
qr += window.btoa(decoder.decode(committedStaticKey));
let qr = encode(applicationName) + ":";
qr += encode(applicationVersion) + ":";
qr += encode(shardId) + ":";
qr += fromUint8Array(ephemeralKey) + ":";
qr += fromUint8Array(committedStaticKey);
return qr;
}
@ -81,12 +83,11 @@ export function fromQr(qr: string): {
if (values.length != 5) throw new Error("invalid qr string");
const encoder = new TextEncoder();
const applicationName = window.atob(values[0]);
const applicationVersion = window.atob(values[1]);
const shardId = window.atob(values[2]);
const ephemeralKey = encoder.encode(window.atob(values[3]));
const committedStaticKey = encoder.encode(window.atob(values[4]));
const applicationName = decode(values[0]);
const applicationVersion = decode(values[1]);
const shardId = decode(values[2]);
const ephemeralKey = toUint8Array(values[3]);
const committedStaticKey = toUint8Array(values[4]);
return { applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey };
}

View File

@ -0,0 +1,225 @@
import { HMACDRBG } from "@stablelib/hmac-drbg";
import { randomBytes } from "@stablelib/random";
import { expect } from "chai";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { NoiseHandshakeDecoder, NoiseHandshakeEncoder } from "./codec";
import { commitPublicKey, generateX25519KeyPair } from "./crypto";
import { Handshake } from "./handshake";
import { NoiseHandshakePatterns } from "./patterns";
import { MessageNametagLength } from "./payload";
import { NoisePublicKey } from "./publickey";
import { fromQr, toQr } from "./utils";
describe("Waku Noise Sessions", () => {
const rng = new HMACDRBG();
// This test implements the Device pairing and Secure Transfers with Noise
// detailed in the 43/WAKU2-DEVICE-PAIRING RFC https://rfc.vac.dev/spec/43/
it("Noise Waku Pairing Handhshake and Secure transfer", async function () {
// Pairing Phase
// ==========
const hsPattern = NoiseHandshakePatterns.WakuPairing;
// Alice static/ephemeral key initialization and commitment
const aliceStaticKey = generateX25519KeyPair();
const aliceEphemeralKey = generateX25519KeyPair();
const s = randomBytes(32, rng);
const aliceCommittedStaticKey = commitPublicKey(aliceStaticKey.publicKey, s);
// Bob static/ephemeral key initialization and commitment
const bobStaticKey = generateX25519KeyPair();
const bobEphemeralKey = generateX25519KeyPair();
const r = randomBytes(32, rng);
const bobCommittedStaticKey = commitPublicKey(bobStaticKey.publicKey, r);
// Content topic information
const applicationName = "waku-noise-sessions";
const applicationVersion = "0.1";
const shardId = "10";
const qrMessageNameTag = randomBytes(MessageNametagLength, rng);
// Out-of-band Communication
// Bob prepares the QR and sends it out-of-band to Alice
const qr = toQr(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey);
const enc = new TextEncoder();
const qrBytes = enc.encode(qr);
// Alice deserializes the QR code
const readQR = fromQr(qr);
// We check if QR serialization/deserialization works
expect(readQR.applicationName).to.be.equals(applicationName);
expect(readQR.applicationVersion).to.be.equals(applicationVersion);
expect(readQR.shardId).to.be.equals(shardId);
expect(uint8ArrayEquals(bobEphemeralKey.publicKey, readQR.ephemeralKey)).to.be.true;
expect(uint8ArrayEquals(bobCommittedStaticKey, readQR.committedStaticKey)).to.be.true;
// We set the contentTopic from the content topic parameters exchanged in the QR
const contentTopic =
"/" + applicationName + "/" + applicationVersion + "/wakunoise/1/sessions_shard-" + shardId + "/proto";
// Pre-handshake message
// <- eB {H(sB||r), contentTopicParams, messageNametag}
const preMessagePKs = [NoisePublicKey.fromPublicKey(bobEphemeralKey.publicKey)];
// We initialize the Handshake states.
// Note that we pass the whole qr serialization as prologue information
const aliceHS = new Handshake({
hsPattern,
ephemeralKey: aliceEphemeralKey,
staticKey: aliceStaticKey,
prologue: qrBytes,
preMessagePKs,
initiator: true,
});
const bobHS = new Handshake({
hsPattern,
ephemeralKey: bobEphemeralKey,
staticKey: bobStaticKey,
prologue: qrBytes,
preMessagePKs,
});
// Pairing Handshake
// ==========
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// -> eA, eAeB {H(sA||s)} [authcode]
// The messageNametag for the first handshake message is randomly generated and exchanged out-of-band
// and corresponds to qrMessageNametag
// We set the transport message to be H(sA||s)
let sentTransportMessage = aliceCommittedStaticKey;
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
// The message is sent with a messageNametag equal to the one received through the QR code
let aliceStep = aliceHS.stepHandshake({
transportMessage: sentTransportMessage,
messageNametag: qrMessageNameTag,
});
let encoder = new NoiseHandshakeEncoder(contentTopic, aliceStep);
// We prepare a Waku message from Alice's payload2
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
let wakuMsgBytes = await encoder.encode({});
// We decode the WakuMessage from the ProtoBuffer
let decoder = new NoiseHandshakeDecoder(contentTopic);
let wakuMsgProto = await decoder.decodeProto(wakuMsgBytes!);
let v2Msg = await decoder.decode(wakuMsgProto!);
expect(v2Msg!.contentTopic).to.be.equals(contentTopic);
expect(v2Msg?.payloadV2.equals(aliceStep.payload2)).to.be.true;
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
// Note that Bob verifies if the received payloadv2 has the expected messageNametag set
let bobStep = bobHS.stepHandshake({ readPayloadV2: v2Msg?.payloadV2, messageNametag: qrMessageNameTag });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage));
// We generate an authorization code using the handshake state
const aliceAuthcode = aliceHS.genAuthcode();
const bobAuthcode = bobHS.genAuthcode();
// We check that they are equal. Note that this check has to be confirmed with a user interaction.
expect(aliceAuthcode).to.be.equals(bobAuthcode);
// 2nd step
// <- sB, eAsB {r}
// Alice and Bob update their local next messageNametag using the available handshake information
// During the handshake, messageNametag = HKDF(h), where h is the handshake hash value at the end of the last processed message
let aliceMessageNametag = aliceHS.hs.toMessageNametag();
let bobMessageNametag = bobHS.hs.toMessageNametag();
// We set as a transport message the commitment randomness r
sentTransportMessage = r;
// At this step, Bob writes and returns a payload
bobStep = bobHS.stepHandshake({ transportMessage: sentTransportMessage, messageNametag: bobMessageNametag });
// We prepare a Waku message from Bob's payload2
encoder = new NoiseHandshakeEncoder(contentTopic, bobStep);
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
wakuMsgBytes = await encoder.encode({});
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder(contentTopic);
wakuMsgProto = await decoder.decodeProto(wakuMsgBytes!);
v2Msg = await decoder.decode(wakuMsgProto!);
expect(v2Msg?.payloadV2.equals(bobStep.payload2)).to.be.true;
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS.stepHandshake({ readPayloadV2: v2Msg?.payloadV2, messageNametag: aliceMessageNametag });
expect(uint8ArrayEquals(aliceStep.transportMessage, sentTransportMessage));
// Alice further checks if Bob's commitment opens to Bob's static key she just received
const expectedBobCommittedStaticKey = commitPublicKey(aliceHS.hs.rs!, aliceStep.transportMessage);
expect(uint8ArrayEquals(expectedBobCommittedStaticKey, bobCommittedStaticKey)).to.be.true;
// 3rd step
// -> sA, sAeB, sAsB {s}
// Alice and Bob update their local next messageNametag using the available handshake information
aliceMessageNametag = aliceHS.hs.toMessageNametag();
bobMessageNametag = bobHS.hs.toMessageNametag();
// We set as a transport message the commitment randomness s
sentTransportMessage = s;
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS.stepHandshake({ transportMessage: sentTransportMessage, messageNametag: aliceMessageNametag });
// We prepare a Waku message from Alice's payload2
encoder = new NoiseHandshakeEncoder(contentTopic, aliceStep);
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
wakuMsgBytes = await encoder.encode({});
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder(contentTopic);
wakuMsgProto = await decoder.decodeProto(wakuMsgBytes!);
v2Msg = await decoder.decode(wakuMsgProto!);
expect(v2Msg?.payloadV2.equals(aliceStep.payload2)).to.be.true;
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS.stepHandshake({ readPayloadV2: v2Msg?.payloadV2, messageNametag: bobMessageNametag });
expect(uint8ArrayEquals(bobStep.transportMessage, sentTransportMessage));
// Bob further checks if Alice's commitment opens to Alice's static key he just received
const expectedAliceCommittedStaticKey = commitPublicKey(bobHS.hs.rs!, bobStep.transportMessage);
expect(uint8ArrayEquals(expectedAliceCommittedStaticKey, aliceCommittedStaticKey)).to.be.true;
// Secure Transfer Phase
// ==========
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
});
});