mirror of https://github.com/waku-org/js-noise.git
feat: pairing object
This commit is contained in:
parent
c911333ef1
commit
b60bc1af1e
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
26
src/index.ts
26
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 };
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue