mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-02 13:43:08 +00:00
291 lines
12 KiB
TypeScript
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");
|
|
}
|
|
});
|
|
});
|