js-noise/src/waku-noise-pairing.spec.ts
2023-11-27 15:21:41 -04:00

291 lines
12 KiB
TypeScript

import { HMACDRBG } from "@stablelib/hmac-drbg";
import { randomBytes } from "@stablelib/random";
import { SHA256 } from "@stablelib/sha256";
import { expect } from "chai";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import {
NoiseHandshakeDecoder,
NoiseHandshakeEncoder,
NoiseSecureTransferDecoder,
NoiseSecureTransferEncoder,
} from "./codec.js";
import { commitPublicKey } from "./crypto.js";
import { DH25519 } from "./dh25519.js";
import { Handshake } from "./handshake.js";
import { MessageNametagBufferSize, MessageNametagLength } from "./messagenametag.js";
import { NoiseHandshakePatterns } from "./patterns.js";
import { NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js";
const PUBSUB_TOPIC = "default";
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 dhKey = new DH25519();
const hash = SHA256;
const hsPattern = NoiseHandshakePatterns.Noise_WakuPairing_25519_ChaChaPoly_SHA256;
// Alice static/ephemeral key initialization and commitment
const aliceStaticKey = dhKey.generateKeyPair();
const aliceEphemeralKey = dhKey.generateKeyPair();
const s = randomBytes(32, rng);
const aliceCommittedStaticKey = commitPublicKey(hash, aliceStaticKey.publicKey, s);
// Bob static/ephemeral key initialization and commitment
const bobStaticKey = dhKey.generateKeyPair();
const bobEphemeralKey = dhKey.generateKeyPair();
const r = randomBytes(32, rng);
const bobCommittedStaticKey = commitPublicKey(hash, 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 = new QR(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey);
// Alice deserializes the QR code
const readQR = QR.from(qr.toString());
// 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: qr.toByteArray(),
preMessagePKs,
initiator: true,
});
const bobHS = new Handshake({
hsPattern,
ephemeralKey: bobEphemeralKey,
staticKey: bobStaticKey,
prologue: qr.toByteArray(),
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.toWire({ payload: new Uint8Array() });
// We decode the WakuMessage from the ProtoBuffer
let decoder = new NoiseHandshakeDecoder(contentTopic);
let wakuMsgProto = await decoder.fromWireToProtoObj(wakuMsgBytes!);
let v2Msg = await decoder.fromProtoObj(PUBSUB_TOPIC, 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.toWire({ payload: new Uint8Array() });
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder(contentTopic);
wakuMsgProto = await decoder.fromWireToProtoObj(wakuMsgBytes!);
v2Msg = await decoder.fromProtoObj(PUBSUB_TOPIC, 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(hash, 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.toWire({ payload: new Uint8Array() });
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder(contentTopic);
wakuMsgProto = await decoder.fromWireToProtoObj(wakuMsgBytes!);
v2Msg = await decoder.fromProtoObj(PUBSUB_TOPIC, 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(hash, bobHS.hs.rs!, bobStep.transportMessage);
expect(uint8ArrayEquals(expectedAliceCommittedStaticKey, aliceCommittedStaticKey)).to.be.true;
// Secure Transfer Phase
// ==========
// We finalize the handshake to retrieve the Inbound/Outbound Symmetric States
const aliceHSResult = aliceHS.finalizeHandshake();
const bobHSResult = bobHS.finalizeHandshake();
const aliceEncoder = new NoiseSecureTransferEncoder(contentTopic, aliceHSResult);
const bobEncoder = new NoiseSecureTransferEncoder(contentTopic, bobHSResult);
const aliceDecoder = new NoiseSecureTransferDecoder(contentTopic, aliceHSResult);
const bobDecoder = new NoiseSecureTransferDecoder(contentTopic, bobHSResult);
// We test read/write of random messages exchanged between Alice and Bob
// Note that we exchange more than the number of messages contained in the nametag buffer to test if they are filled correctly as the communication proceeds
for (let i = 0; i < 10 * MessageNametagBufferSize; i++) {
// Alice writes to Bob
let message = randomBytes(32, rng);
let encodedMsg = await aliceEncoder.toWire({ payload: message });
let readMessageProto = await bobDecoder.fromWireToProtoObj(encodedMsg!);
let readMessage = await bobDecoder.fromProtoObj(PUBSUB_TOPIC, readMessageProto!);
expect(uint8ArrayEquals(message, readMessage!.payload)).to.be.true;
// Bob writes to Alice
message = randomBytes(32, rng);
encodedMsg = await bobEncoder.toWire({ payload: message });
readMessageProto = await aliceDecoder.fromWireToProtoObj(encodedMsg!);
readMessage = await aliceDecoder.fromProtoObj(PUBSUB_TOPIC, readMessageProto!);
expect(uint8ArrayEquals(message, readMessage!.payload)).to.be.true;
}
// We test how nametag buffers help in detecting lost messages
// Alice writes two messages to Bob, but only the second is received
let message = randomBytes(32, rng);
let payload2 = aliceHSResult.writeMessage(message);
message = randomBytes(32, rng);
payload2 = aliceHSResult.writeMessage(message);
try {
bobHSResult.readMessage(payload2);
expect(false, "should not reach here").to.be.true;
} catch (err) {
let message;
if (err instanceof Error) message = err.message;
else message = String(err);
expect(message).to.be.equals("nametag is not ok");
}
// We adjust bob nametag buffer for next test (i.e. the missed message is correctly recovered)
bobHSResult.nametagsInbound.delete(2);
message = randomBytes(32, rng);
payload2 = bobHSResult.writeMessage(message);
const readMessage = aliceHSResult.readMessage(payload2);
expect(uint8ArrayEquals(message, readMessage)).to.be.true;
// We test if a missing nametag is correctly detected
message = randomBytes(32, rng);
payload2 = aliceHSResult.writeMessage(message);
bobHSResult.nametagsInbound.delete(1);
try {
bobHSResult.readMessage(payload2);
} catch (err) {
let message;
if (err instanceof Error) message = err.message;
else message = String(err);
expect(message).to.be.equals("nametag is not ok");
}
});
});