From b60bc1af1eb926d37ecbbe7496cdafee7e5a87b2 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Sat, 3 Dec 2022 09:38:08 -0400 Subject: [PATCH] feat: pairing object --- package-lock.json | 64 ++++++--- package.json | 2 + src/handshake.ts | 20 ++- src/index.spec.ts | 2 +- src/index.ts | 26 ++-- src/pairing.spec.ts | 108 +++++++++++++++ src/pairing.ts | 310 ++++++++++++++++++++++++++++++++++++++++++++ src/patterns.ts | 2 +- src/payload.ts | 7 +- 9 files changed, 503 insertions(+), 38 deletions(-) create mode 100644 src/pairing.spec.ts create mode 100644 src/pairing.ts diff --git a/package-lock.json b/package-lock.json index 649f942..46dbffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@waku/noise", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@waku/noise", - "version": "0.0.1", + "version": "0.0.2", "license": "Apache-2.0 OR MIT", "dependencies": { "@stablelib/chacha20poly1305": "^1.0.1", @@ -16,7 +16,9 @@ "@stablelib/sha256": "^1.0.1", "@stablelib/x25519": "^1.0.1", "bn.js": "^5.2.1", + "eventemitter3": "^5.0.0", "js-base64": "^3.7.3", + "p-event": "^5.0.1", "pkcs7-padding": "^0.1.1", "uint8arraylist": "^2.3.2", "uint8arrays": "^4.0.2" @@ -4785,9 +4787,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -5485,10 +5487,9 @@ "dev": true }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.0.tgz", + "integrity": "sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg==" }, "node_modules/events": { "version": "3.3.0", @@ -6415,6 +6416,12 @@ "node": ">= 6" } }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8932,7 +8939,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/p-event/-/p-event-5.0.1.tgz", "integrity": "sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==", - "dev": true, "dependencies": { "p-timeout": "^5.0.2" }, @@ -8947,7 +8953,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", - "dev": true, "engines": { "node": ">=12" }, @@ -9035,6 +9040,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/p-queue/node_modules/p-timeout": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", @@ -15743,9 +15754,9 @@ } }, "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -16263,10 +16274,9 @@ "dev": true }, "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.0.tgz", + "integrity": "sha512-riuVbElZZNXLeLEoprfNYoDSwTBRR44X3mnhdI1YcnENpWTCsTTVZ2zFuqQcpoyqPQIUXdiPEU0ECAq0KQRaHg==" }, "events": { "version": "3.3.0", @@ -16942,6 +16952,14 @@ "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + } } }, "http-proxy-agent": { @@ -18868,7 +18886,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/p-event/-/p-event-5.0.1.tgz", "integrity": "sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==", - "dev": true, "requires": { "p-timeout": "^5.0.2" }, @@ -18876,8 +18893,7 @@ "p-timeout": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", - "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", - "dev": true + "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==" } } }, @@ -18936,6 +18952,12 @@ "p-timeout": "^5.0.2" }, "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "p-timeout": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", diff --git a/package.json b/package.json index 423e9aa..fc1cf00 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,9 @@ "@stablelib/sha256": "^1.0.1", "@stablelib/x25519": "^1.0.1", "bn.js": "^5.2.1", + "eventemitter3": "^5.0.0", "js-base64": "^3.7.3", + "p-event": "^5.0.1", "pkcs7-padding": "^0.1.1", "uint8arraylist": "^2.3.2", "uint8arrays": "^4.0.2" diff --git a/src/handshake.ts b/src/handshake.ts index ec61975..bae70b5 100644 --- a/src/handshake.ts +++ b/src/handshake.ts @@ -128,6 +128,13 @@ export interface StepHandshakeParameters { messageNametag?: Uint8Array; } +export class MessageNametagError extends Error { + constructor(public readonly expectedNametag: Uint8Array, public readonly actualNametag: Uint8Array) { + super("the message nametag of the read message doesn't match the expected one"); + this.name = "MessageNametagError"; + } +} + export class Handshake { hs: HandshakeState; constructor({ @@ -198,13 +205,13 @@ export class Handshake { // If we write an answer at this handshake step if (writing) { // 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]; - } catch (err) { + const protocolId = PayloadV2ProtocolIDs[this.hs.handshakePattern.name]; + if (protocolId === undefined) { throw new Error("handshake pattern not supported"); } + hsStepResult.payload2.protocolId = protocolId; + // We set the messageNametag and the handshake and transport messages hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag); hsStepResult.payload2.handshakeMessage = this.hs.processMessagePatternTokens(); @@ -218,8 +225,9 @@ export class Handshake { // 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 (!uint8ArrayEquals(readPayloadV2.messageNametag, toMessageNametag(messageNametag))) { - throw new Error("the message nametag of the read message doesn't match the expected one"); + const expectedNametag = toMessageNametag(messageNametag); + if (!uint8ArrayEquals(readPayloadV2.messageNametag, expectedNametag)) { + throw new MessageNametagError(expectedNametag, readPayloadV2.messageNametag); } // We process the read public keys and (eventually decrypt) the read transport message diff --git a/src/index.spec.ts b/src/index.spec.ts index e0f2c5b..e447727 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -36,7 +36,7 @@ function randomNoisePublicKey(): NoisePublicKey { function randomPayloadV2(rng: HMACDRBG): PayloadV2 { const messageNametag = randomBytes(MessageNametagLength, rng); - const protocolId = Math.floor(Math.random() * 255); + const protocolId = 14; const handshakeMessage = [randomNoisePublicKey(), randomNoisePublicKey(), randomNoisePublicKey()]; const transportMessage = randomBytes(128); return new PayloadV2(messageNametag, protocolId, handshakeMessage, transportMessage); diff --git a/src/index.ts b/src/index.ts index 5ed925e..3430f3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,17 @@ import { NoiseHandshakeEncoder, NoiseSecureTransferDecoder, NoiseSecureTransferEncoder, -} from "./codec"; -import { generateX25519KeyPair, generateX25519KeyPairFromSeed } from "./crypto"; +} from "./codec.js"; +import { generateX25519KeyPair, generateX25519KeyPairFromSeed } from "./crypto.js"; import { Handshake, HandshakeParameters, HandshakeResult, HandshakeStepResult, + MessageNametagError, StepHandshakeParameters, -} from "./handshake"; +} from "./handshake.js"; +import { InitiatorParameters, Receiver, ReceiverParameters, Sender, WakuPairing } from "./pairing.js"; import { EmptyPreMessage, HandshakePattern, @@ -21,12 +23,19 @@ import { NoiseTokens, PayloadV2ProtocolIDs, PreMessagePattern, -} from "./patterns"; -import { MessageNametagBuffer } from "./payload"; -import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey"; -import { QR } from "./qr"; +} from "./patterns.js"; +import { MessageNametagBuffer } from "./payload.js"; +import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js"; +import { QR } from "./qr.js"; -export { Handshake, HandshakeParameters, HandshakeResult, HandshakeStepResult, StepHandshakeParameters }; +export { + Handshake, + HandshakeParameters, + HandshakeResult, + HandshakeStepResult, + MessageNametagError, + StepHandshakeParameters, +}; export { generateX25519KeyPair, generateX25519KeyPairFromSeed }; export { EmptyPreMessage, @@ -42,3 +51,4 @@ export { ChaChaPolyCipherState, NoisePublicKey }; export { MessageNametagBuffer }; export { NoiseHandshakeDecoder, NoiseHandshakeEncoder, NoiseSecureTransferDecoder, NoiseSecureTransferEncoder }; export { QR }; +export { InitiatorParameters, ReceiverParameters, Sender, Receiver, WakuPairing }; diff --git a/src/pairing.spec.ts b/src/pairing.spec.ts new file mode 100644 index 0000000..bcd13bf --- /dev/null +++ b/src/pairing.spec.ts @@ -0,0 +1,108 @@ +import { HMACDRBG } from "@stablelib/hmac-drbg"; +import { randomBytes } from "@stablelib/random"; +import { expect } from "chai"; +import { EventEmitter } from "eventemitter3"; +import { Decoder, Encoder, Message } from "js-waku/lib/interfaces"; +import { pEvent } from "p-event"; +import { equals as uint8ArrayEquals } from "uint8arrays/equals"; + +import { NoiseHandshakeMessage } from "./codec"; +import { generateX25519KeyPair } from "./crypto"; +import { ReceiverParameters, WakuPairing } from "./pairing"; +import { MessageNametagBufferSize } from "./payload"; + +describe("js-noise: pairing object", () => { + const rng = new HMACDRBG(); + + const confirmAuthCodeFlow = async function (pairingObj: WakuPairing, shouldConfirm: boolean): Promise { + const authCode = await pairingObj.getAuthCode(); + console.log("Authcode: ", authCode); // TODO: compare that authCode is the same in both confirmation flows + pairingObj.validateAuthCode(shouldConfirm); + }; + + // ================= + // Simulate waku. This code is not meant to be used IRL + const msgEmitter = new EventEmitter(); + const sender = { + async publish(encoder: Encoder, msg: Message): Promise { + const protoMsg = await encoder.encodeProto(msg); + msgEmitter.emit(encoder.contentTopic, protoMsg); + }, + }; + const decoderMap: { [key: string]: Decoder } = {}; + const receiver = { + subscribe(decoder: Decoder): void { + decoderMap[decoder.contentTopic] = decoder; + }, + async nextMessage(contentTopic: string): Promise { + const msg = await pEvent(msgEmitter, contentTopic); + const decodedMessage = await decoderMap[contentTopic].decode(msg); + return decodedMessage!; + }, + }; + // ================= + + it("should pair", async function () { + const bobStaticKey = generateX25519KeyPair(); + const aliceStaticKey = generateX25519KeyPair(); + + const recvParameters = new ReceiverParameters(); + const bobPairingObj = new WakuPairing(sender, receiver, bobStaticKey, recvParameters); + const bobExecP1 = bobPairingObj.execute(); + + // Confirmation is done by manually + confirmAuthCodeFlow(bobPairingObj, true); + + const initParameters = bobPairingObj.getPairingInfo(); + const alicePairingObj = new WakuPairing(sender, receiver, aliceStaticKey, initParameters); + const aliceExecP1 = alicePairingObj.execute(); + + // Confirmation is done manually + confirmAuthCodeFlow(alicePairingObj, true); + + const [bobCodecs, aliceCodecs] = await Promise.all([bobExecP1, aliceExecP1]); + + const bobEncoder = bobCodecs[0]; + const bobDecoder = bobCodecs[1]; + const aliceEncoder = aliceCodecs[0]; + const aliceDecoder = aliceCodecs[1]; + + // 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.encode({ payload: message }); + let readMessageProto = await bobDecoder.decodeProto(encodedMsg!); + let readMessage = await bobDecoder.decode(readMessageProto!); + + expect(uint8ArrayEquals(message, readMessage!.payload)).to.be.true; + + // Bob writes to Alice + message = randomBytes(32, rng); + encodedMsg = await bobEncoder.encode({ payload: message }); + readMessageProto = await aliceDecoder.decodeProto(encodedMsg!); + readMessage = await aliceDecoder.decode(readMessageProto!); + + expect(uint8ArrayEquals(message, readMessage!.payload)).to.be.true; + } + }); + + it("should timeout", async function () { + const bobPairingObj = new WakuPairing(sender, receiver, generateX25519KeyPair(), new ReceiverParameters()); + const alicePairingObj = new WakuPairing(sender, receiver, generateX25519KeyPair(), bobPairingObj.getPairingInfo()); + + const bobExecP1 = bobPairingObj.execute(1000); + const aliceExecP1 = alicePairingObj.execute(1000); + + try { + await Promise.all([bobExecP1, aliceExecP1]); + 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("pairing has timed out"); + } + }); +}); diff --git a/src/pairing.ts b/src/pairing.ts new file mode 100644 index 0000000..9c8f44d --- /dev/null +++ b/src/pairing.ts @@ -0,0 +1,310 @@ +import { HMACDRBG } from "@stablelib/hmac-drbg"; +import { randomBytes } from "@stablelib/random"; +import { EventEmitter } from "eventemitter3"; +import { Decoder, Encoder, Message } from "js-waku/lib/interfaces"; +import { pEvent } from "p-event"; +import { equals as uint8ArrayEquals } from "uint8arrays/equals"; + +import { KeyPair } from "./@types/keypair.js"; +import { + NoiseHandshakeDecoder, + NoiseHandshakeEncoder, + NoiseHandshakeMessage, + NoiseSecureTransferDecoder, + NoiseSecureTransferEncoder, +} from "./codec"; +import { commitPublicKey, generateX25519KeyPair } from "./crypto"; +import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake"; +import { NoiseHandshakePatterns } from "./patterns"; +import { MessageNametagLength } from "./payload"; +import { NoisePublicKey } from "./publickey"; +import { QR } from "./qr"; + +export interface Sender { + publish(encoder: Encoder, msg: Message): Promise; +} + +export interface Receiver { + subscribe(decoder: Decoder): void; + + // next message should return messages received in a content topic + // messages should be kept in a queue, meaning that nextMessage + // will call pop in the queue to remove the oldest message received + // (it's important to maintain order of received messages) + nextMessage(contentTopic: string): Promise; +} + +const rng = new HMACDRBG(); + +export class InitiatorParameters { + constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {} +} + +export class ReceiverParameters { + constructor( + public readonly applicationName: string = "waku-noise-sessions", + public readonly applicationVersion: string = "0.1", + public readonly shardId: string = "10" + ) {} +} + +export class WakuPairing { + public readonly contentTopic: string; + + private initiator: boolean; + private randomFixLenVal: Uint8Array; // r or s depending on who is sending the message + private handshake: Handshake; + private myCommittedStaticKey: Uint8Array; + private qr: QR; + private qrMessageNameTag: Uint8Array; + private authCode?: string; + private started = false; + + private eventEmitter = new EventEmitter(); + + private static toContentTopic(qr: QR): string { + return ( + "/" + qr.applicationName + "/" + qr.applicationVersion + "/wakunoise/1/sessions_shard-" + qr.shardId + "/proto" + ); + } + + constructor( + private sender: Sender, + private receiver: Receiver, + private myStaticKey: KeyPair, + pairingParameters: InitiatorParameters | ReceiverParameters, + private myEphemeralKey: KeyPair = generateX25519KeyPair() + ) { + this.randomFixLenVal = randomBytes(32, rng); + this.myCommittedStaticKey = commitPublicKey(this.myStaticKey.publicKey, this.randomFixLenVal); + + if (pairingParameters instanceof InitiatorParameters) { + this.initiator = true; + this.qr = QR.fromString(pairingParameters.qrCode); + this.qrMessageNameTag = pairingParameters.qrMessageNameTag; + } else { + this.initiator = false; + this.qrMessageNameTag = randomBytes(MessageNametagLength, rng); + this.qr = new QR( + pairingParameters.applicationName, + pairingParameters.applicationVersion, + pairingParameters.shardId, + this.myEphemeralKey.publicKey, + this.myCommittedStaticKey + ); + } + // We set the contentTopic from the content topic parameters exchanged in the QR + this.contentTopic = WakuPairing.toContentTopic(this.qr); + + // Pre-handshake message + // <- eB {H(sB||r), contentTopicParams, messageNametag} + const preMessagePKs = [NoisePublicKey.fromPublicKey(this.qr.ephemeralKey)]; + + this.handshake = new Handshake({ + hsPattern: NoiseHandshakePatterns.WakuPairing, + ephemeralKey: myEphemeralKey, + staticKey: myStaticKey, + prologue: this.qr.toByteArray(), + preMessagePKs, + initiator: this.initiator, + }); + } + + public getPairingInfo(): InitiatorParameters { + return new InitiatorParameters(this.qr.toString(), this.qrMessageNameTag); + } + + public async getAuthCode(): Promise { + return new Promise((resolve) => { + if (this.authCode) { + resolve(this.authCode); + } else { + this.eventEmitter.on("authCodeGenerated", (authCode: string) => { + this.authCode = authCode; + resolve(authCode); + }); + } + }); + } + + public validateAuthCode(confirmed: boolean): void { + this.eventEmitter.emit("confirmAuthCode", confirmed); + } + + private async isAuthCodeConfirmed(): Promise { + // wait for user to confirm or not, or for the whole pairing process to time out + const p1 = pEvent(this.eventEmitter, "confirmAuthCode"); + const p2 = pEvent(this.eventEmitter, "pairingTimeout"); + return Promise.race([p1, p2]); + } + + private async executeReadStepWithNextMessage( + contentTopic: string, + messageNametag: Uint8Array + ): Promise { + // TODO: create test unit for this function + let stopLoop = false; + + this.eventEmitter.once("pairingTimeout", () => { + stopLoop = true; + }); + + this.eventEmitter.once("pairingComplete", () => { + stopLoop = true; + }); + + while (!stopLoop) { + try { + const hsMessage = await this.receiver.nextMessage(contentTopic); + const step = this.handshake.stepHandshake({ + readPayloadV2: hsMessage.payloadV2, + messageNametag, + }); + return step; + } catch (err) { + if (err instanceof MessageNametagError) { + console.error("Unexpected message nametag", err.expectedNametag, err.actualNametag); + } + } + } + + throw new Error("could not obtain next message"); + } + + private async initiatorHandshake(): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> { + // The handshake initiator writes a Waku2 payload v2 containing the handshake message + // and the (encrypted) transport message + // The message is sent with a messageNametag equal to the one received through the QR code + let hsStep = this.handshake.stepHandshake({ + transportMessage: this.myCommittedStaticKey, + messageNametag: this.qrMessageNameTag, + }); + + // We prepare a message from initiator's payload2 + // At this point wakuMsg is sent over the Waku network to receiver content topic + let encoder = new NoiseHandshakeEncoder(this.contentTopic, hsStep); + await this.sender.publish(encoder, {}); + + // We generate an authorization code using the handshake state + // this check has to be confirmed with a user interaction, comparing auth codes in both ends + this.eventEmitter.emit("authCodeGenerated", this.handshake.genAuthcode()); + + const confirmed = await this.isAuthCodeConfirmed(); + if (!confirmed) { + throw new Error("authcode is not confirmed"); + } + + // 2nd step + // <- sB, eAsB {r} + hsStep = await this.executeReadStepWithNextMessage(this.contentTopic, this.handshake.hs.toMessageNametag()); + + if (!this.handshake.hs.rs) throw new Error("invalid handshake state"); + + // Initiator further checks if receiver's commitment opens to receiver's static key received + const expectedReceiverCommittedStaticKey = commitPublicKey(this.handshake.hs.rs, hsStep.transportMessage); + if (!uint8ArrayEquals(expectedReceiverCommittedStaticKey, this.qr.committedStaticKey)) { + throw new Error("expected committed static key does not match the receiver actual committed static key"); + } + + // 3rd step + // -> sA, sAeB, sAsB {s} + // Similarly as in first step, the initiator writes a Waku2 payload containing the handshake message and the (encrypted) transport message + hsStep = this.handshake.stepHandshake({ + transportMessage: this.randomFixLenVal, + messageNametag: this.handshake.hs.toMessageNametag(), + }); + + encoder = new NoiseHandshakeEncoder(this.contentTopic, hsStep); + await this.sender.publish(encoder, {}); + + // Secure Transfer Phase + const hsResult = this.handshake.finalizeHandshake(); + + this.eventEmitter.emit("pairingComplete"); + + return this.getSecureCodec(hsResult); + } + + private async receiverHandshake(): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> { + // Subscribe to the contact content topic + const decoder = new NoiseHandshakeDecoder(this.contentTopic); + this.receiver.subscribe(decoder); + + // the received reads the initiator's payloads, and returns the (decrypted) transport message the initiator sent + // Note that the received verifies if the received payloadV2 has the expected messageNametag set + let hsStep = await this.executeReadStepWithNextMessage(this.contentTopic, this.qrMessageNameTag); + + const initiatorCommittedStaticKey = new Uint8Array(hsStep.transportMessage); + + this.eventEmitter.emit("authCodeGenerated", this.handshake.genAuthcode()); + + const confirmed = await this.isAuthCodeConfirmed(); + if (!confirmed) { + throw new Error("authcode is not confirmed"); + } + + // 2nd step + // <- sB, eAsB {r} + // Receiver writes and returns a payload + hsStep = this.handshake.stepHandshake({ + transportMessage: this.randomFixLenVal, + messageNametag: this.handshake.hs.toMessageNametag(), + }); + + // We prepare a Waku message from receiver's payload2 + const encoder = new NoiseHandshakeEncoder(this.contentTopic, hsStep); + await this.sender.publish(encoder, {}); + + // 3rd step + // -> sA, sAeB, sAsB {s} + + // The receiver reads the initiator's payload sent by the initiator + hsStep = await this.executeReadStepWithNextMessage(this.contentTopic, this.handshake.hs.toMessageNametag()); + + if (!this.handshake.hs.rs) throw new Error("invalid handshake state"); + + // The receiver further checks if the initiator's commitment opens to the initiator's static key received + const expectedInitiatorCommittedStaticKey = commitPublicKey(this.handshake.hs.rs, hsStep.transportMessage); + if (!uint8ArrayEquals(expectedInitiatorCommittedStaticKey, initiatorCommittedStaticKey)) { + throw new Error("expected committed static key does not match the initiator actual committed static key"); + } + + // Secure Transfer Phase + const hsResult = this.handshake.finalizeHandshake(); + + this.eventEmitter.emit("pairingComplete"); + + return this.getSecureCodec(hsResult); + } + + private getSecureCodec(hsResult: HandshakeResult): [NoiseSecureTransferEncoder, NoiseSecureTransferDecoder] { + const secureEncoder = new NoiseSecureTransferEncoder(this.contentTopic, hsResult); + const secureDecoder = new NoiseSecureTransferDecoder(this.contentTopic, hsResult); + + return [secureEncoder, secureDecoder]; + } + + async execute(timeoutMs = 30000): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> { + if (this.started) { + throw new Error("pairing already executed. Create new pairing object"); + } + + this.started = true; + return new Promise((resolve, reject) => { + // Limit QR exposure to 30s + const timer = setTimeout(() => { + reject(new Error("pairing has timed out")); + this.eventEmitter.emit("pairingTimeout"); + }, timeoutMs); + + const handshakeFn = this.initiator ? this.initiatorHandshake : this.receiverHandshake; + handshakeFn + .bind(this)() + .then( + (response) => resolve(response), + (err) => reject(new Error(err)) + ) + .finally(() => clearTimeout(timer)); + }); + } +} diff --git a/src/patterns.ts b/src/patterns.ts index 44b8d44..c7c8827 100644 --- a/src/patterns.ts +++ b/src/patterns.ts @@ -118,7 +118,7 @@ export const NoiseHandshakePatterns = { // Supported Protocol ID for PayloadV2 objects // Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification -export const PayloadV2ProtocolIDs = { +export const PayloadV2ProtocolIDs: { [id: string]: number } = { "": 0, Noise_K1K1_25519_ChaChaPoly_SHA256: 10, Noise_XK1_25519_ChaChaPoly_SHA256: 11, diff --git a/src/payload.ts b/src/payload.ts index 34f30d4..979a803 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -7,6 +7,7 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { MessageNametag } from "./@types/handshake"; import { ChachaPolyTagLen, Curve25519KeySize, hashSHA256 } from "./crypto"; +import { PayloadV2ProtocolIDs } from "./patterns"; import { NoisePublicKey } from "./publickey"; import { readUIntLE, writeUIntLE } from "./utils"; @@ -217,8 +218,12 @@ export class PayloadV2 { i += MessageNametagLength; // We read the Protocol ID - // TODO: when the list of supported protocol ID is defined, check if read protocol ID is supported const protocolId = payload[i]; + const protocolName = Object.keys(PayloadV2ProtocolIDs).find((key) => PayloadV2ProtocolIDs[key] === protocolId); + if (protocolName === undefined) { + throw new Error("protocolId not found"); + } + i++; // We read the Handshake Message length (1 byte)