mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-10 17:43:07 +00:00
test: waku noise sessions
This commit is contained in:
parent
aa2490825b
commit
0fb4bb3183
@ -15,7 +15,8 @@
|
||||
"HASHLEN",
|
||||
"ciphertext",
|
||||
"preshared",
|
||||
"libp2p"
|
||||
"libp2p",
|
||||
"Authcode"
|
||||
],
|
||||
"flagWords": [],
|
||||
"ignorePaths": [
|
||||
|
||||
5374
package-lock.json
generated
5374
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
100
src/codec.ts
Normal 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<> {}
|
||||
*/
|
||||
@ -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
|
||||
|
||||
25
src/utils.ts
25
src/utils.ts
@ -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 };
|
||||
}
|
||||
|
||||
225
src/waku-noise-pairing.spec.ts
Normal file
225
src/waku-noise-pairing.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user