feat: pairing object

This commit is contained in:
Richard Ramos 2022-12-03 09:38:08 -04:00 committed by RichΛrd
parent c911333ef1
commit b60bc1af1e
9 changed files with 503 additions and 38 deletions

64
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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 };

108
src/pairing.spec.ts Normal file
View File

@ -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<void> {
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<void> {
const protoMsg = await encoder.encodeProto(msg);
msgEmitter.emit(encoder.contentTopic, protoMsg);
},
};
const decoderMap: { [key: string]: Decoder<NoiseHandshakeMessage> } = {};
const receiver = {
subscribe(decoder: Decoder<NoiseHandshakeMessage>): void {
decoderMap[decoder.contentTopic] = decoder;
},
async nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage> {
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");
}
});
});

310
src/pairing.ts Normal file
View File

@ -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<void>;
}
export interface Receiver {
subscribe(decoder: Decoder<NoiseHandshakeMessage>): 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<NoiseHandshakeMessage>;
}
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<string> {
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<boolean | undefined> {
// 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<boolean | undefined>([p1, p2]);
}
private async executeReadStepWithNextMessage(
contentTopic: string,
messageNametag: Uint8Array
): Promise<HandshakeStepResult> {
// 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));
});
}
}

View File

@ -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,

View File

@ -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)