chore: add docs

This commit is contained in:
Richard Ramos 2023-01-06 13:34:32 -04:00
parent 6299732797
commit e2634cc6c2
No known key found for this signature in database
GPG Key ID: 1CE87DB518195760
21 changed files with 772 additions and 319 deletions

View File

@ -14,6 +14,7 @@
"blocksize", "blocksize",
"Nametag", "Nametag",
"Cipherstate", "Cipherstate",
"cipherstates",
"Nametags", "Nametags",
"HASHLEN", "HASHLEN",
"ciphertext", "ciphertext",

49
README.md Normal file
View File

@ -0,0 +1,49 @@
# js-noise
Browser library using Noise Protocols for Waku Payload Encryption
https://rfc.vac.dev/spec/35/
### Install
```
npm install @waku/noise
# or with yarn
yarn add @waku/noise
```
### Documentation
Refer to the specs and examples for details on how to use this library
### Running example app
```
git clone https://github.com/waku-org/js-noise
cd js-noise/example
npm install # or yarn
npm start
```
Browse http://localhost:8080 to see the webapp where the pairing process can be initiated
## Bugs, Questions & Features
If you encounter any bug or would like to propose new features, feel free to [open an issue](https://github.com/waku-org/js-rln/issues/new/).
For more general discussion, help and latest news, join [Vac Discord](https://discord.gg/PQFdubGt6d) or [Telegram](https://t.me/vacp2p).
## License
Licensed and distributed under either of
- MIT license: [LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT
or
- Apache License, Version 2.0, ([LICENSE-APACHEv2](LICENSE-APACHEv2) or http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except according to those terms.

View File

@ -82,7 +82,7 @@ function getSenderAndResponder(node) {
await subscriptions.get(contentTopic)(); await subscriptions.get(contentTopic)();
subscriptions.delete(contentTopic); subscriptions.delete(contentTopic);
} else { } else {
console.log("Subscriptipon doesnt exist") console.log("Subscription doesnt exist");
} }
}, },
}; };

View File

@ -1,4 +1,3 @@
export type bytes = Uint8Array;
export type bytes32 = Uint8Array; export type bytes32 = Uint8Array;
export type bytes16 = Uint8Array; export type bytes16 = Uint8Array;

View File

@ -1,6 +1,2 @@
import type { bytes } from "./basic.js";
export type Hkdf = [bytes, bytes, bytes];
// a transport message (for Noise handshakes and ChaChaPoly encryptions) // a transport message (for Noise handshakes and ChaChaPoly encryptions)
export type MessageNametag = Uint8Array; export type MessageNametag = Uint8Array;

View File

@ -10,9 +10,28 @@ const log = debug("waku:message:noise-codec");
const OneMillion = BigInt(1_000_000); const OneMillion = BigInt(1_000_000);
export const Version = 2; // WakuMessage version for noise protocol
const version = 2;
/**
* Used internally in the pairing object to represent a handshake message
*/
export class NoiseHandshakeMessage extends MessageV0 implements Message {
get payloadV2(): PayloadV2 {
if (!this.payload) throw new Error("no payload available");
return PayloadV2.deserialize(this.payload);
}
}
/**
* Used in the pairing object for encoding the messages exchanged
* during the handshake process
*/
export class NoiseHandshakeEncoder implements Encoder { export class NoiseHandshakeEncoder implements Encoder {
/**
* @param contentTopic content topic on which the encoded WakuMessages will be sent
* @param hsStepResult the result of a step executed while performing the handshake process
*/
constructor(public contentTopic: string, private hsStepResult: HandshakeStepResult) {} constructor(public contentTopic: string, private hsStepResult: HandshakeStepResult) {}
async encode(message: Message): Promise<Uint8Array | undefined> { async encode(message: Message): Promise<Uint8Array | undefined> {
@ -25,34 +44,21 @@ export class NoiseHandshakeEncoder implements Encoder {
const timestamp = message.timestamp ?? new Date(); const timestamp = message.timestamp ?? new Date();
return { return {
payload: this.hsStepResult.payload2.serialize(), payload: this.hsStepResult.payload2.serialize(),
version: Version, version: version,
contentTopic: this.contentTopic, contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion, timestamp: BigInt(timestamp.valueOf()) * OneMillion,
}; };
} }
} }
export class NoiseHandshakeMessage extends MessageV0 implements Message { /**
get payloadV2(): PayloadV2 { * Used in the pairing object for decoding the messages exchanged
if (!this.payload) throw new Error("no payload available"); * during the handshake process
return PayloadV2.deserialize(this.payload); */
}
}
export class NoiseSecureMessage extends MessageV0 implements Message {
private readonly _decodedPayload: Uint8Array;
constructor(proto: proto_message.WakuMessage, decodedPayload: Uint8Array) {
super(proto);
this._decodedPayload = decodedPayload;
}
get payload(): Uint8Array {
return this._decodedPayload;
}
}
export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> { export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
/**
* @param contentTopic content topic on which the encoded WakuMessages were sent
*/
constructor(public contentTopic: string) {} constructor(public contentTopic: string) {}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> { decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
@ -67,8 +73,8 @@ export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
proto.version = 0; proto.version = 0;
} }
if (proto.version !== Version) { if (proto.version !== version) {
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version); log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
@ -81,7 +87,34 @@ export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
} }
} }
/**
* Represents a secure message. These are messages that are transmitted
* after a successful handshake is performed.
*/
export class NoiseSecureMessage extends MessageV0 implements Message {
private readonly _decodedPayload: Uint8Array;
constructor(proto: proto_message.WakuMessage, decodedPayload: Uint8Array) {
super(proto);
this._decodedPayload = decodedPayload;
}
get payload(): Uint8Array {
return this._decodedPayload;
}
}
/**
* js-waku encoder for secure messages. After a handshake is successful, a
* codec for encoding messages is generated. The messages encoded with this
* codec will be encrypted with the cipherstates and message nametags that were
* created after a handshake is complete
*/
export class NoiseSecureTransferEncoder implements Encoder { export class NoiseSecureTransferEncoder implements Encoder {
/**
* @param contentTopic content topic on which the encoded WakuMessages were sent
* @param hsResult handshake result obtained after the handshake is successful
*/
constructor(public contentTopic: string, private hsResult: HandshakeResult) {} constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
async encode(message: Message): Promise<Uint8Array | undefined> { async encode(message: Message): Promise<Uint8Array | undefined> {
@ -103,14 +136,24 @@ export class NoiseSecureTransferEncoder implements Encoder {
return { return {
payload, payload,
version: Version, version: version,
contentTopic: this.contentTopic, contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion, timestamp: BigInt(timestamp.valueOf()) * OneMillion,
}; };
} }
} }
/**
* js-waku decoder for secure messages. After a handshake is successful, a codec
* for decoding messages is generated. This decoder will attempt to decrypt
* messages with the cipherstates and message nametags that were created after a
* handshake is complete
*/
export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> { export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
/**
* @param contentTopic content topic on which the encoded WakuMessages were sent
* @param hsResult handshake result obtained after the handshake is successful
*/
constructor(public contentTopic: string, private hsResult: HandshakeResult) {} constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> { decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
@ -125,8 +168,8 @@ export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
proto.version = 0; proto.version = 0;
} }
if (proto.version !== Version) { if (proto.version !== version) {
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version); log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }

View File

@ -1,20 +1,32 @@
import { ChaCha20Poly1305, TAG_LENGTH } from "@stablelib/chacha20poly1305"; import { ChaCha20Poly1305, TAG_LENGTH } from "@stablelib/chacha20poly1305";
import { HKDF } from "@stablelib/hkdf"; import { HKDF as hkdf } from "@stablelib/hkdf";
import { hash, SHA256 } from "@stablelib/sha256"; import { hash, SHA256 } from "@stablelib/sha256";
import * as x25519 from "@stablelib/x25519"; import * as x25519 from "@stablelib/x25519";
import { concat as uint8ArrayConcat } from "uint8arrays/concat"; import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import type { bytes, bytes32 } from "./@types/basic.js"; import type { bytes32 } from "./@types/basic.js";
import type { Hkdf } from "./@types/handshake.js";
import type { KeyPair } from "./@types/keypair.js"; import type { KeyPair } from "./@types/keypair.js";
export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH; export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH;
export const ChachaPolyTagLen = TAG_LENGTH; export const ChachaPolyTagLen = TAG_LENGTH;
/**
* Generate hash using SHA2-256
* @param data data to hash
* @returns hash digest
*/
export function hashSHA256(data: Uint8Array): Uint8Array { export function hashSHA256(data: Uint8Array): Uint8Array {
return hash(data); return hash(data);
} }
/**
* Convert an Uint8Array into a 32-byte value. If the input data length is different
* from 32, throw an error. This is used mostly as a validation function to ensure
* that an Uint8Array represents a valid x25519 key
* @param s input data
* @return 32-byte key
*/
export function intoCurve25519Key(s: Uint8Array): bytes32 { export function intoCurve25519Key(s: Uint8Array): bytes32 {
if (s.length != x25519.PUBLIC_KEY_LENGTH) { if (s.length != x25519.PUBLIC_KEY_LENGTH) {
throw new Error("invalid public key length"); throw new Error("invalid public key length");
@ -23,20 +35,29 @@ export function intoCurve25519Key(s: Uint8Array): bytes32 {
return s; return s;
} }
export function getHKDF(ck: bytes32, ikm: Uint8Array): Hkdf { /**
const okm = getHKDFRaw(ck, ikm, 96); * HKDF key derivation function using SHA256
const k1 = okm.subarray(0, 32); * @param ck chaining key
const k2 = okm.subarray(32, 64); * @param ikm input key material
const k3 = okm.subarray(64, 96); * @param length length of each generated key
* @param numKeys number of keys to generate
return [k1, k2, k3]; * @returns array of `numValues` length containing Uint8Array keys of a given byte `length`
} */
export function HKDF(ck: bytes32, ikm: Uint8Array, length: number, numKeys: number): Array<Uint8Array> {
export function getHKDFRaw(ck: bytes32, ikm: Uint8Array, numBytes: number): Uint8Array { const numBytes = length * numKeys;
const hkdf = new HKDF(SHA256, ikm, ck); const okm = new hkdf(SHA256, ikm, ck).expand(numBytes);
return hkdf.expand(numBytes); const result = [];
for (let i = 0; i < numBytes; i += length) {
const k = okm.subarray(i, i + length);
result.push(k);
}
return result;
} }
/**
* Generate a random keypair
* @returns Keypair
*/
export function generateX25519KeyPair(): KeyPair { export function generateX25519KeyPair(): KeyPair {
const keypair = x25519.generateKeyPair(); const keypair = x25519.generateKeyPair();
@ -46,7 +67,12 @@ export function generateX25519KeyPair(): KeyPair {
}; };
} }
export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair { /**
* Generate x25519 keypair using an input seed
* @param seed 32-byte secret
* @returns Keypair
*/
export function generateX25519KeyPairFromSeed(seed: bytes32): KeyPair {
const keypair = x25519.generateKeyPairFromSeed(seed); const keypair = x25519.generateKeyPairFromSeed(seed);
return { return {
@ -55,30 +81,52 @@ export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
}; };
} }
export function generateX25519SharedKey(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array { /**
return x25519.sharedKey(privateKey, publicKey); * Encrypt and authenticate data using ChaCha20-Poly1305
} * @param plaintext data to encrypt
* @param nonce 12 byte little-endian nonce
export function chaCha20Poly1305Encrypt(plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes { * @param ad associated data
* @param k 32-byte key
* @returns sealed ciphertext including authentication tag
*/
export function chaCha20Poly1305Encrypt(
plaintext: Uint8Array,
nonce: Uint8Array,
ad: Uint8Array,
k: bytes32
): Uint8Array {
const ctx = new ChaCha20Poly1305(k); const ctx = new ChaCha20Poly1305(k);
return ctx.seal(nonce, plaintext, ad); return ctx.seal(nonce, plaintext, ad);
} }
/**
* Authenticate and decrypt data using ChaCha20-Poly1305
* @param ciphertext data to decrypt
* @param nonce 12 byte little-endian nonce
* @param ad associated data
* @param k 32-byte key
* @returns plaintext if decryption was successful, `null` otherwise
*/
export function chaCha20Poly1305Decrypt( export function chaCha20Poly1305Decrypt(
ciphertext: Uint8Array, ciphertext: Uint8Array,
nonce: Uint8Array, nonce: Uint8Array,
ad: Uint8Array, ad: Uint8Array,
k: bytes32 k: bytes32
): bytes | null { ): Uint8Array | null {
const ctx = new ChaCha20Poly1305(k); const ctx = new ChaCha20Poly1305(k);
return ctx.open(nonce, ciphertext, ad); return ctx.open(nonce, ciphertext, ad);
} }
/**
* Perform a DiffieHellman key exchange
* @param privateKey x25519 private key
* @param publicKey x25519 public key
* @returns shared secret
*/
export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 { export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
try { try {
const derivedU8 = generateX25519SharedKey(privateKey, publicKey); const derivedU8 = x25519.sharedKey(privateKey, publicKey);
if (derivedU8.length === 32) { if (derivedU8.length === 32) {
return derivedU8; return derivedU8;
@ -91,7 +139,12 @@ export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
} }
} }
// Commits a public key pk for randomness r as H(pk || s) /**
* Generates a random static key commitment using a public key pk for randomness r as H(pk || s)
* @param publicKey x25519 public key
* @param r random fixed-length value
* @returns 32 byte hash
*/
export function commitPublicKey(publicKey: bytes32, r: Uint8Array): bytes32 { export function commitPublicKey(publicKey: bytes32, r: Uint8Array): bytes32 {
return hashSHA256(uint8ArrayConcat([publicKey, r])); return hashSHA256(uint8ArrayConcat([publicKey, r]));
} }

View File

@ -4,11 +4,12 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic"; import { bytes32 } from "./@types/basic";
import { KeyPair } from "./@types/keypair"; import { KeyPair } from "./@types/keypair";
import { getHKDFRaw } from "./crypto.js"; import { HKDF } from "./crypto";
import { HandshakeState, NoisePaddingBlockSize } from "./handshake_state.js"; import { HandshakeState, NoisePaddingBlockSize } from "./handshake_state.js";
import { MessageNametagBuffer, toMessageNametag } from "./messagenametag";
import { CipherState } from "./noise.js"; import { CipherState } from "./noise.js";
import { HandshakePattern, PayloadV2ProtocolIDs } from "./patterns.js"; import { HandshakePattern, PayloadV2ProtocolIDs } from "./patterns.js";
import { MessageNametagBuffer, PayloadV2, toMessageNametag } from "./payload.js"; import { PayloadV2 } from "./payload.js";
import { NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
// Noise state machine // Noise state machine
@ -172,7 +173,7 @@ export class Handshake {
// Generates an 8 decimal digits authorization code using HKDF and the handshake state // Generates an 8 decimal digits authorization code using HKDF and the handshake state
genAuthcode(): string { genAuthcode(): string {
const output0 = getHKDFRaw(this.hs.ss.h, new Uint8Array(), 8); const [output0] = HKDF(this.hs.ss.h, new Uint8Array(), 8, 1);
const bn = new BN(output0); const bn = new BN(output0);
const code = bn.mod(new BN(100_000_000)).toString().padStart(8, "0"); const code = bn.mod(new BN(100_000_000)).toString().padStart(8, "0");
return code.toString(); return code.toString();

View File

@ -5,10 +5,10 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic.js"; import { bytes32 } from "./@types/basic.js";
import { MessageNametag } from "./@types/handshake.js"; import { MessageNametag } from "./@types/handshake.js";
import type { KeyPair } from "./@types/keypair.js"; import type { KeyPair } from "./@types/keypair.js";
import { Curve25519KeySize, dh, generateX25519KeyPair, getHKDF, intoCurve25519Key } from "./crypto.js"; import { Curve25519KeySize, dh, generateX25519KeyPair, HKDF, intoCurve25519Key } from "./crypto.js";
import { MessageNametagLength } from "./messagenametag";
import { SymmetricState } from "./noise.js"; import { SymmetricState } from "./noise.js";
import { EmptyPreMessage, HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js"; import { HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js";
import { MessageNametagLength } from "./payload.js";
import { NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
const log = debug("waku:noise:handshake-state"); const log = debug("waku:noise:handshake-state");
@ -101,14 +101,15 @@ export class HandshakeState {
} }
genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } { genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } {
const [nms1, nms2] = getHKDF(this.ss.h, new Uint8Array()); const [nms1, nms2] = HKDF(this.ss.h, new Uint8Array(), 2, 32);
return { nms1, nms2 }; return { nms1, nms2 };
} }
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag // Uses the cryptographic information stored in the input handshake state to generate a random message nametag
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented // In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
toMessageNametag(): MessageNametag { toMessageNametag(): MessageNametag {
const [output] = getHKDF(this.ss.h, new Uint8Array()); console.log("HELLO!");
const [output] = HKDF(this.ss.h, new Uint8Array(), 32, 1);
return output.subarray(0, MessageNametagLength); return output.subarray(0, MessageNametagLength);
} }
@ -181,7 +182,7 @@ export class HandshakeState {
// We retrieve the pre-message patterns to process, if any // We retrieve the pre-message patterns to process, if any
// If none, there's nothing to do // If none, there's nothing to do
if (this.handshakePattern.preMessagePatterns == EmptyPreMessage) { if (this.handshakePattern.preMessagePatterns.length == 0) {
return; return;
} }

View File

@ -5,10 +5,11 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto"; import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
import { Handshake, HandshakeStepResult } from "./handshake"; import { Handshake, HandshakeStepResult } from "./handshake";
import { MessageNametagBuffer, MessageNametagLength } from "./messagenametag";
import { CipherState, createEmptyKey, SymmetricState } from "./noise"; import { CipherState, createEmptyKey, SymmetricState } from "./noise";
import { MAX_NONCE, Nonce } from "./nonce"; import { MAX_NONCE, Nonce } from "./nonce";
import { NoiseHandshakePatterns } from "./patterns"; import { NoiseHandshakePatterns } from "./patterns";
import { MessageNametagBuffer, MessageNametagLength, PayloadV2 } from "./payload"; import { PayloadV2 } from "./payload";
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey"; import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey";
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState { function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {

View File

@ -13,9 +13,9 @@ import {
MessageNametagError, MessageNametagError,
StepHandshakeParameters, StepHandshakeParameters,
} from "./handshake.js"; } from "./handshake.js";
import { MessageNametagBuffer } from "./messagenametag.js";
import { InitiatorParameters, Responder, ResponderParameters, Sender, WakuPairing } from "./pairing.js"; import { InitiatorParameters, Responder, ResponderParameters, Sender, WakuPairing } from "./pairing.js";
import { import {
EmptyPreMessage,
HandshakePattern, HandshakePattern,
MessageDirection, MessageDirection,
MessagePattern, MessagePattern,
@ -24,7 +24,6 @@ import {
PayloadV2ProtocolIDs, PayloadV2ProtocolIDs,
PreMessagePattern, PreMessagePattern,
} from "./patterns.js"; } from "./patterns.js";
import { MessageNametagBuffer } from "./payload.js";
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js"; import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js"; import { QR } from "./qr.js";
@ -38,7 +37,6 @@ export {
}; };
export { generateX25519KeyPair, generateX25519KeyPairFromSeed }; export { generateX25519KeyPair, generateX25519KeyPairFromSeed };
export { export {
EmptyPreMessage,
HandshakePattern, HandshakePattern,
MessageDirection, MessageDirection,
MessagePattern, MessagePattern,

126
src/messagenametag.ts Normal file
View File

@ -0,0 +1,126 @@
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { MessageNametag } from "./@types/handshake.js";
import { hashSHA256 } from "./crypto.js";
import { writeUIntLE } from "./utils.js";
export const MessageNametagLength = 16;
export const MessageNametagBufferSize = 50;
/**
* Converts a sequence or array (arbitrary size) to a MessageNametag
* @param input
* @returns
*/
export function toMessageNametag(input: Uint8Array): MessageNametag {
return input.subarray(0, MessageNametagLength);
}
export class MessageNametagBuffer {
private buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
private counter = 0;
secret?: Uint8Array;
constructor() {
for (let i = 0; i < this.buffer.length; i++) {
this.buffer[i] = new Uint8Array(MessageNametagLength);
}
}
/**
* Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n )
*/
initNametagsBuffer(): void {
// We default the counter and buffer fields
this.counter = 0;
this.buffer = new Array<MessageNametag>(MessageNametagBufferSize);
if (this.secret) {
for (let i = 0; i < this.buffer.length; i++) {
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[i] = toMessageNametag(d);
this.counter++;
}
} else {
// We warn users if no secret is set
console.debug("The message nametags buffer has not a secret set");
}
}
/**
* Pop the nametag from the message nametag buffer
* @returns MessageNametag
*/
pop(): MessageNametag {
// Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned
const messageNametag = new Uint8Array(this.buffer[0]);
this.delete(1);
return messageNametag;
}
/**
* Checks if the input messageNametag is contained in the input MessageNametagBuffer
* @param messageNametag Message nametag to verify
* @returns true if it's the expected nametag, false otherwise
*/
checkNametag(messageNametag: MessageNametag): boolean {
const index = this.buffer.findIndex((x) => uint8ArrayEquals(x, messageNametag));
if (index == -1) {
console.debug("Message nametag not found in buffer");
return false;
} else if (index > 0) {
console.debug(
"Message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost"
);
return false;
}
// index is 0, hence the read message tag is the next expected one
return true;
}
private rotateLeft(k: number): void {
if (k < 0 || this.buffer.length == 0) {
return;
}
const idx = this.buffer.length - (k % this.buffer.length);
const a1 = this.buffer.slice(idx);
const a2 = this.buffer.slice(0, idx);
this.buffer = a1.concat(a2);
}
/**
* Deletes the first n elements in buffer and appends n new ones
* @param n number of message nametags to delete
*/
delete(n: number): void {
if (n <= 0) {
return;
}
// We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced)
n = Math.min(n, MessageNametagBufferSize);
// We update the last n values in the array if a secret is set
// Note that if the input MessageNametagBuffer is set to default, nothing is done here
if (this.secret) {
// We rotate left the array by n
this.rotateLeft(n);
for (let i = 0; i < n; i++) {
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[this.buffer.length - n + i] = toMessageNametag(d);
this.counter++;
}
} else {
// We warn users that no secret is set
console.debug("The message nametags buffer has no secret set");
}
}
}

View File

@ -4,7 +4,7 @@ import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import type { bytes32 } from "./@types/basic.js"; import type { bytes32 } from "./@types/basic.js";
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, getHKDF, hashSHA256 } from "./crypto.js"; import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, hashSHA256, HKDF } from "./crypto.js";
import { Nonce } from "./nonce.js"; import { Nonce } from "./nonce.js";
import { HandshakePattern } from "./patterns.js"; import { HandshakePattern } from "./patterns.js";
@ -38,43 +38,74 @@ const log = debug("waku:noise:handshake-state");
################################# #################################
*/ */
/**
* Create empty chaining key
* @returns 32-byte empty key
*/
export function createEmptyKey(): bytes32 { export function createEmptyKey(): bytes32 {
return new Uint8Array(32); return new Uint8Array(32);
} }
/**
* Checks if a 32-byte key is empty
* @param k key to verify
* @returns true if empty, false otherwise
*/
export function isEmptyKey(k: bytes32): boolean { export function isEmptyKey(k: bytes32): boolean {
const emptyKey = createEmptyKey(); const emptyKey = createEmptyKey();
return uint8ArrayEquals(emptyKey, k); return uint8ArrayEquals(emptyKey, k);
} }
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object /**
// Contains an encryption key k and a nonce n (used in Noise as a counter) * The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
* Contains an encryption key k and a nonce n (used in Noise as a counter)
*/
export class CipherState { export class CipherState {
k: bytes32; k: bytes32;
// For performance reasons, the nonce is represented as a Nonce object // For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits. // The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce; n: Nonce;
/**
* @param k encryption key
* @param n nonce
*/
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) { constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) {
this.k = k; this.k = k;
this.n = n; this.n = n;
} }
/**
* Create a copy of the CipherState
* @returns a copy of the CipherState
*/
clone(): CipherState { clone(): CipherState {
return new CipherState(new Uint8Array(this.k), new Nonce(this.n.getUint64())); return new CipherState(new Uint8Array(this.k), new Nonce(this.n.getUint64()));
} }
equals(b: CipherState): boolean { /**
return uint8ArrayEquals(this.k, b.getKey()) && this.n.getUint64() == b.getNonce().getUint64(); * Check CipherState equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: CipherState): boolean {
return uint8ArrayEquals(this.k, other.getKey()) && this.n.getUint64() == other.getNonce().getUint64();
} }
// Checks if a Cipher State has an encryption key set /**
* Checks if a Cipher State has an encryption key set
* @returns true if a key is set, false otherwise`
*/
protected hasKey(): boolean { protected hasKey(): boolean {
return !isEmptyKey(this.k); return !isEmptyKey(this.k);
} }
// Encrypts a plaintext using key material in a Noise Cipher State /**
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one * Encrypts a plaintext using key material in a Noise Cipher State
* The CipherState is updated increasing the nonce (used as a counter in Noise) by one
* @param ad associated data
* @param plaintext data to encrypt
*/
encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array { encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
this.n.assertValue(); this.n.assertValue();
@ -96,8 +127,12 @@ export class CipherState {
return ciphertext; return ciphertext;
} }
// Decrypts a ciphertext using key material in a Noise Cipher State /**
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one * Decrypts a ciphertext using key material in a Noise Cipher State
* The CipherState is updated increasing the nonce (used as a counter in Noise) by one
* @param ad associated data
* @param ciphertext data to decrypt
*/
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array { decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
this.n.assertValue(); this.n.assertValue();
@ -119,27 +154,44 @@ export class CipherState {
} }
} }
// Sets the nonce of a Cipher State /**
* Sets the nonce of a Cipher State
* @param nonce Nonce
*/
setNonce(nonce: Nonce): void { setNonce(nonce: Nonce): void {
this.n = nonce; this.n = nonce;
} }
// Sets the key of a Cipher State /**
* Sets the key of a Cipher State
* @param key set the cipherstate encryption key
*/
setCipherStateKey(key: bytes32): void { setCipherStateKey(key: bytes32): void {
this.k = key; this.k = key;
} }
// Gets the key of a Cipher State /**
* Gets the encryption key of a Cipher State
* @returns encryption key
*/
getKey(): bytes32 { getKey(): bytes32 {
return this.k; return this.k;
} }
// Gets the nonce of a Cipher State /**
* Gets the nonce of a Cipher State
* @returns Nonce
*/
getNonce(): Nonce { getNonce(): Nonce {
return this.n; return this.n;
} }
} }
/**
* Hash protocol name
* @param name name of the noise handshake pattern to hash
* @returns sha256 digest of the protocol name
*/
function hashProtocol(name: string): Uint8Array { function hashProtocol(name: string): Uint8Array {
// If protocol_name is less than or equal to HASHLEN bytes in length, // If protocol_name is less than or equal to HASHLEN bytes in length,
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes. // sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
@ -155,8 +207,10 @@ function hashProtocol(name: string): Uint8Array {
} }
} }
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h * The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
* Contains a Cipher State cs, the chaining key ck and the handshake hash value h
*/
export class SymmetricState { export class SymmetricState {
cs: CipherState; cs: CipherState;
h: bytes32; // handshake hash h: bytes32; // handshake hash
@ -169,15 +223,24 @@ export class SymmetricState {
this.hsPattern = hsPattern; this.hsPattern = hsPattern;
} }
equals(b: SymmetricState): boolean { /**
* Check CipherState equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: SymmetricState): boolean {
return ( return (
this.cs.equals(b.cs) && this.cs.equals(other.cs) &&
uint8ArrayEquals(this.ck, b.ck) && uint8ArrayEquals(this.ck, other.ck) &&
uint8ArrayEquals(this.h, b.h) && uint8ArrayEquals(this.h, other.h) &&
this.hsPattern.equals(b.hsPattern) this.hsPattern.equals(other.hsPattern)
); );
} }
/**
* Create a copy of the SymmetricState
* @returns a copy of the SymmetricState
*/
clone(): SymmetricState { clone(): SymmetricState {
const ss = new SymmetricState(this.hsPattern); const ss = new SymmetricState(this.hsPattern);
ss.cs = this.cs.clone(); ss.cs = this.cs.clone();
@ -186,30 +249,39 @@ export class SymmetricState {
return ss; return ss;
} }
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Updates a Symmetric state chaining key and symmetric state * MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Updates a Symmetric state chaining key and symmetric state
* @param inputKeyMaterial
*/
mixKey(inputKeyMaterial: Uint8Array): void { mixKey(inputKeyMaterial: Uint8Array): void {
// We derive two keys using HKDF // We derive two keys using HKDF
const [ck, tempK] = getHKDF(this.ck, inputKeyMaterial); const [ck, tempK] = HKDF(this.ck, inputKeyMaterial, 32, 2);
// We update ck and the Cipher state's key k using the output of HDKF // We update ck and the Cipher state's key k using the output of HDKF
this.cs = new CipherState(tempK); this.cs = new CipherState(tempK);
this.ck = ck; this.ck = ck;
log("mixKey", this.ck, this.cs.k); log("mixKey", this.ck, this.cs.k);
} }
// MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Hashes data into a Symmetric State's handshake hash value h * MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Hashes data into a Symmetric State's handshake hash value h
* @param data input data to hash into h
*/
mixHash(data: Uint8Array): void { mixHash(data: Uint8Array): void {
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value // We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
this.h = hashSHA256(uint8ArrayConcat([this.h, data])); this.h = hashSHA256(uint8ArrayConcat([this.h, data]));
log("mixHash", this.h); log("mixHash", this.h);
} }
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Combines MixKey and MixHash * mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Combines MixKey and MixHash
* @param inputKeyMaterial
*/
mixKeyAndHash(inputKeyMaterial: Uint8Array): void { mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
// Derives 3 keys using HKDF, the chaining key and the input key material // Derives 3 keys using HKDF, the chaining key and the input key material
const [tmpKey0, tmpKey1, tmpKey2] = getHKDF(this.ck, inputKeyMaterial); const [tmpKey0, tmpKey1, tmpKey2] = HKDF(this.ck, inputKeyMaterial, 32, 3);
// Sets the chaining key // Sets the chaining key
this.ck = tmpKey0; this.ck = tmpKey0;
// Updates the handshake hash value // Updates the handshake hash value
@ -219,9 +291,14 @@ export class SymmetricState {
this.cs = new CipherState(tmpKey2); this.cs = new CipherState(tmpKey2);
} }
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Combines encryptWithAd and mixHash * EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag) * Combines encryptWithAd and mixHash
* Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad
* specified by Noise (can be used to authenticate messageNametag)
* @param plaintext data to encrypt
* @param extraAd extra additional data
*/
encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array { encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
// The additional data // The additional data
const ad = uint8ArrayConcat([this.h, extraAd]); const ad = uint8ArrayConcat([this.h, extraAd]);
@ -233,8 +310,12 @@ export class SymmetricState {
return ciphertext; return ciphertext;
} }
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Combines decryptWithAd and mixHash * DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Combines decryptWithAd and mixHash
* @param ciphertext data to decrypt
* @param extraAd extra additional data
*/
decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array { decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
// The additional data // The additional data
const ad = uint8ArrayConcat([this.h, extraAd]); const ad = uint8ArrayConcat([this.h, extraAd]);
@ -246,11 +327,14 @@ export class SymmetricState {
return plaintext; return plaintext;
} }
// Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object /**
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages * Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
* @returns CipherState to encrypt and CipherState to decrypt
*/
split(): { cs1: CipherState; cs2: CipherState } { split(): { cs1: CipherState; cs2: CipherState } {
// Derives 2 keys using HKDF and the chaining key // Derives 2 keys using HKDF and the chaining key
const [tmpKey1, tmpKey2] = getHKDF(this.ck, new Uint8Array(0)); const [tmpKey1, tmpKey2] = HKDF(this.ck, new Uint8Array(0), 32, 2);
// Returns a tuple of two Cipher States initialized with the derived keys // Returns a tuple of two Cipher States initialized with the derived keys
return { return {
cs1: new CipherState(tmpKey1), cs1: new CipherState(tmpKey1),
@ -258,17 +342,26 @@ export class SymmetricState {
}; };
} }
// Gets the chaining key field of a Symmetric State /**
* Gets the chaining key field of a Symmetric State
* @returns Chaining key
*/
getChainingKey(): bytes32 { getChainingKey(): bytes32 {
return this.ck; return this.ck;
} }
// Gets the handshake hash field of a Symmetric State /**
* Gets the handshake hash field of a Symmetric State
* @returns Handshake hash
*/
getHandshakeHash(): bytes32 { getHandshakeHash(): bytes32 {
return this.h; return this.h;
} }
// Gets the Cipher State field of a Symmetric State /**
* Gets the Cipher State field of a Symmetric State
* @returns Cipher State
*/
getCipherState(): CipherState { getCipherState(): CipherState {
return this.cs; return this.cs;
} }

View File

@ -1,4 +1,6 @@
import type { bytes, uint64 } from "./@types/basic.js"; // Adapted from https://github.com/ChainSafe/js-libp2p-noise/blob/master/src/nonce.ts
import type { uint64 } from "./@types/basic.js";
export const MIN_NONCE = 0; export const MIN_NONCE = 0;
// For performance reasons, the nonce is represented as a JS `number` // For performance reasons, the nonce is represented as a JS `number`
@ -17,7 +19,7 @@ const ERR_MAX_NONCE = "Cipherstate has reached maximum n, a new handshake must b
*/ */
export class Nonce { export class Nonce {
private n: uint64; private n: uint64;
private readonly bytes: bytes; private readonly bytes: Uint8Array;
private readonly view: DataView; private readonly view: DataView;
constructor(n = MIN_NONCE) { constructor(n = MIN_NONCE) {
@ -33,7 +35,7 @@ export class Nonce {
this.view.setUint32(4, this.n, true); this.view.setUint32(4, this.n, true);
} }
getBytes(): bytes { getBytes(): Uint8Array {
return this.bytes; return this.bytes;
} }

View File

@ -8,8 +8,8 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { NoiseHandshakeMessage } from "./codec"; import { NoiseHandshakeMessage } from "./codec";
import { generateX25519KeyPair } from "./crypto"; import { generateX25519KeyPair } from "./crypto";
import { MessageNametagBufferSize } from "./messagenametag";
import { ResponderParameters, WakuPairing } from "./pairing"; import { ResponderParameters, WakuPairing } from "./pairing";
import { MessageNametagBufferSize } from "./payload";
describe("js-noise: pairing object", () => { describe("js-noise: pairing object", () => {
const rng = new HMACDRBG(); const rng = new HMACDRBG();

View File

@ -16,27 +16,48 @@ import {
} from "./codec.js"; } from "./codec.js";
import { commitPublicKey, generateX25519KeyPair } from "./crypto.js"; import { commitPublicKey, generateX25519KeyPair } from "./crypto.js";
import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake.js"; import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake.js";
import { MessageNametagLength } from "./messagenametag.js";
import { NoiseHandshakePatterns } from "./patterns.js"; import { NoiseHandshakePatterns } from "./patterns.js";
import { MessageNametagLength } from "./payload.js";
import { NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js"; import { QR } from "./qr.js";
const log = debug("waku:noise:pairing"); const log = debug("waku:noise:pairing");
/**
* Sender interface that an object must implement so the pairing object can publish noise messages
*/
export interface Sender { export interface Sender {
/**
* Publish a message
* @param encoder NoiseHandshakeEncoder encoder to use to encrypt the messages
* @param msg message to broadcast
*/
publish(encoder: Encoder, msg: Message): Promise<void>; publish(encoder: Encoder, msg: Message): Promise<void>;
} }
/**
* Responder interface than an object must implement so the pairing object can receive noise messages
*/
export interface Responder { export interface Responder {
/**
* subscribe to receive the messages from a content topic
* @param decoder Decoder to use to decrypt the NoiseHandshakeMessages
*/
subscribe(decoder: Decoder<NoiseHandshakeMessage>): Promise<void>; subscribe(decoder: Decoder<NoiseHandshakeMessage>): Promise<void>;
// next message should return messages received in a content topic /**
// messages should be kept in a queue, meaning that nextMessage * should return messages received in a content topic
// will call pop in the queue to remove the oldest message received * messages should be kept in a queue, meaning that nextMessage
// (it's important to maintain order of received messages) * will call pop in the queue to remove the oldest message received
* (it's important to maintain order of received messages)
* @param contentTopic content topic to get the next message from
*/
nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage>; nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage>;
// this should stop the subscription /**
* Stop the subscription to the content topic
* @param contentTopic
*/
stop(contentTopic: string): Promise<void>; stop(contentTopic: string): Promise<void>;
} }
@ -46,10 +67,16 @@ function delay(ms: number): Promise<void> {
const rng = new HMACDRBG(); const rng = new HMACDRBG();
/**
* Initiator parameters used to setup the pairing object
*/
export class InitiatorParameters { export class InitiatorParameters {
constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {} constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {}
} }
/**
* Responder parameters used to setup the pairing object
*/
export class ResponderParameters { export class ResponderParameters {
constructor( constructor(
public readonly applicationName: string = "waku-noise-sessions", public readonly applicationName: string = "waku-noise-sessions",
@ -58,6 +85,9 @@ export class ResponderParameters {
) {} ) {}
} }
/**
* Pairing object to setup a noise session
*/
export class WakuPairing { export class WakuPairing {
public readonly contentTopic: string; public readonly contentTopic: string;
@ -73,12 +103,24 @@ export class WakuPairing {
private eventEmitter = new EventEmitter(); private eventEmitter = new EventEmitter();
/**
* Convert a QR into a content topic
* @param qr
* @returns content topic string
*/
private static toContentTopic(qr: QR): string { private static toContentTopic(qr: QR): string {
return ( return (
"/" + qr.applicationName + "/" + qr.applicationVersion + "/wakunoise/1/sessions_shard-" + qr.shardId + "/proto" "/" + qr.applicationName + "/" + qr.applicationVersion + "/wakunoise/1/sessions_shard-" + qr.shardId + "/proto"
); );
} }
/**
* @param sender object that implements Sender interface to publish waku messages
* @param responder object that implements Responder interface to subscribe and receive waku messages
* @param myStaticKey x25519 keypair
* @param pairingParameters Pairing parameters (depending if this is the initiator or responder)
* @param myEphemeralKey optional ephemeral key
*/
constructor( constructor(
private sender: Sender, private sender: Sender,
private responder: Responder, private responder: Responder,
@ -91,7 +133,7 @@ export class WakuPairing {
if (pairingParameters instanceof InitiatorParameters) { if (pairingParameters instanceof InitiatorParameters) {
this.initiator = true; this.initiator = true;
this.qr = QR.fromString(pairingParameters.qrCode); this.qr = QR.from(pairingParameters.qrCode);
this.qrMessageNameTag = pairingParameters.qrMessageNameTag; this.qrMessageNameTag = pairingParameters.qrMessageNameTag;
} else { } else {
this.initiator = false; this.initiator = false;
@ -104,6 +146,7 @@ export class WakuPairing {
this.myCommittedStaticKey this.myCommittedStaticKey
); );
} }
// We set the contentTopic from the content topic parameters exchanged in the QR // We set the contentTopic from the content topic parameters exchanged in the QR
this.contentTopic = WakuPairing.toContentTopic(this.qr); this.contentTopic = WakuPairing.toContentTopic(this.qr);
@ -121,10 +164,19 @@ export class WakuPairing {
}); });
} }
/**
* Get pairing information (as an InitiatorParameter object)
* @returns InitiatorParameters
*/
public getPairingInfo(): InitiatorParameters { public getPairingInfo(): InitiatorParameters {
return new InitiatorParameters(this.qr.toString(), this.qrMessageNameTag); return new InitiatorParameters(this.qr.toString(), this.qrMessageNameTag);
} }
/**
* Get auth code (to validate that pairing). It must be displayed on both
* devices and the user(s) must confirm if the auth code match
* @returns Promise that resolves to an auth code
*/
public async getAuthCode(): Promise<string> { public async getAuthCode(): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.authCode) { if (this.authCode) {
@ -138,8 +190,14 @@ export class WakuPairing {
}); });
} }
public validateAuthCode(confirmed: boolean): void { /**
this.eventEmitter.emit("confirmAuthCode", confirmed); * Indicate if auth code is valid. This is a function that must be
* manually called by the user(s) if the auth code in both devices being
* paired match. If false, pairing session is terminated
* @param isValid true if authcode is correct, false otherwise.
*/
public validateAuthCode(isValid: boolean): void {
this.eventEmitter.emit("confirmAuthCode", isValid);
} }
private async isAuthCodeConfirmed(): Promise<boolean | undefined> { private async isAuthCodeConfirmed(): Promise<boolean | undefined> {
@ -301,6 +359,13 @@ export class WakuPairing {
return WakuPairing.getSecureCodec(this.contentTopic, this.handshakeResult); return WakuPairing.getSecureCodec(this.contentTopic, this.handshakeResult);
} }
/**
* Get codecs for encoding/decoding messages in js-waku. This function can be used
* to continue a session using a stored hsResult
* @param contentTopic Content topic for the waku messages
* @param hsResult Noise Pairing result
* @returns an array with [NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]
*/
static getSecureCodec( static getSecureCodec(
contentTopic: string, contentTopic: string,
hsResult: HandshakeResult hsResult: HandshakeResult
@ -311,6 +376,10 @@ export class WakuPairing {
return [secureEncoder, secureDecoder]; return [secureEncoder, secureDecoder];
} }
/**
* Get handshake result
* @returns result of a successful pairing
*/
public getHandshakeResult(): HandshakeResult { public getHandshakeResult(): HandshakeResult {
if (!this.handshakeResult) { if (!this.handshakeResult) {
throw new Error("handshake is not complete"); throw new Error("handshake is not complete");
@ -318,14 +387,19 @@ export class WakuPairing {
return this.handshakeResult; return this.handshakeResult;
} }
async execute(timeoutMs = 30000): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> { /**
* Execute handshake
* @param timeoutMs Timeout in milliseconds after which the pairing session is invalid
* @returns promise that resolves to codecs for encoding/decoding messages in js-waku
*/
async execute(timeoutMs = 60000): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> {
if (this.started) { if (this.started) {
throw new Error("pairing already executed. Create new pairing object"); throw new Error("pairing already executed. Create new pairing object");
} }
this.started = true; this.started = true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Limit QR exposure to 30s // Limit QR exposure to some timeout
const timer = setTimeout(() => { const timer = setTimeout(() => {
reject(new Error("pairing has timed out")); reject(new Error("pairing has timed out"));
this.eventEmitter.emit("pairingTimeout"); this.eventEmitter.emit("pairingTimeout");

View File

@ -1,6 +1,7 @@
// The Noise tokens appearing in Noise (pre)message patterns /**
* The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics * as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
*/
export enum NoiseTokens { export enum NoiseTokens {
e = "e", e = "e",
s = "s", s = "s",
@ -11,42 +12,61 @@ export enum NoiseTokens {
psk = "psk", psk = "psk",
} }
// The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form) /**
// as in http://www.noiseprotocol.org/noise.html#alice-and-bob * The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
* as in http://www.noiseprotocol.org/noise.html#alice-and-bob
*/
export enum MessageDirection { export enum MessageDirection {
r = "->", r = "->",
l = "<-", l = "<-",
} }
// The pre message pattern consisting of a message direction and some Noise tokens, if any. /**
// (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics) * The pre message pattern consisting of a message direction and some Noise tokens, if any.
* (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
*/
export class PreMessagePattern { export class PreMessagePattern {
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {} constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
equals(b: PreMessagePattern): boolean { /**
* Check PreMessagePattern equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: PreMessagePattern): boolean {
return ( return (
this.direction == b.direction && this.direction == other.direction &&
this.tokens.length === b.tokens.length && this.tokens.length === other.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index]) this.tokens.every((val, index) => val === other.tokens[index])
); );
} }
} }
// The message pattern consisting of a message direction and some Noise tokens /**
// All Noise tokens are allowed * The message pattern consisting of a message direction and some Noise tokens
* All Noise tokens are allowed
*/
export class MessagePattern { export class MessagePattern {
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {} constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
equals(b: MessagePattern): boolean { /**
* Check MessagePattern equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: MessagePattern): boolean {
return ( return (
this.direction == b.direction && this.direction == other.direction &&
this.tokens.length === b.tokens.length && this.tokens.length === other.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index]) this.tokens.every((val, index) => val === other.tokens[index])
); );
} }
} }
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns /**
* The handshake pattern object. It stores the handshake protocol name, the
* handshake pre message patterns and the handshake message patterns
*/
export class HandshakePattern { export class HandshakePattern {
constructor( constructor(
public readonly name: string, public readonly name: string,
@ -54,25 +74,29 @@ export class HandshakePattern {
public readonly messagePatterns: Array<MessagePattern> public readonly messagePatterns: Array<MessagePattern>
) {} ) {}
equals(b: HandshakePattern): boolean { /**
if (this.preMessagePatterns.length != b.preMessagePatterns.length) return false; * Check HandshakePattern equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: HandshakePattern): boolean {
if (this.preMessagePatterns.length != other.preMessagePatterns.length) return false;
for (let i = 0; i < this.preMessagePatterns.length; i++) { for (let i = 0; i < this.preMessagePatterns.length; i++) {
if (!this.preMessagePatterns[i].equals(b.preMessagePatterns[i])) return false; if (!this.preMessagePatterns[i].equals(other.preMessagePatterns[i])) return false;
} }
if (this.messagePatterns.length != b.messagePatterns.length) return false; if (this.messagePatterns.length != other.messagePatterns.length) return false;
for (let i = 0; i < this.messagePatterns.length; i++) { for (let i = 0; i < this.messagePatterns.length; i++) {
if (!this.messagePatterns[i].equals(b.messagePatterns[i])) return false; if (!this.messagePatterns[i].equals(other.messagePatterns[i])) return false;
} }
return this.name == b.name; return this.name == other.name;
} }
} }
// Constants (supported protocols) /**
export const EmptyPreMessage = new Array<PreMessagePattern>(); * Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
*/
// Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
export const NoiseHandshakePatterns = { export const NoiseHandshakePatterns = {
K1K1: new HandshakePattern( K1K1: new HandshakePattern(
"Noise_K1K1_25519_ChaChaPoly_SHA256", "Noise_K1K1_25519_ChaChaPoly_SHA256",
@ -95,16 +119,24 @@ export const NoiseHandshakePatterns = {
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]), new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
] ]
), ),
XX: new HandshakePattern("Noise_XX_25519_ChaChaPoly_SHA256", EmptyPreMessage, [ XX: new HandshakePattern(
new MessagePattern(MessageDirection.r, [NoiseTokens.e]), "Noise_XX_25519_ChaChaPoly_SHA256",
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]), [],
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]), [
]), new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
XXpsk0: new HandshakePattern("Noise_XXpsk0_25519_ChaChaPoly_SHA256", EmptyPreMessage, [ new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]), new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]), ]
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]), ),
]), XXpsk0: new HandshakePattern(
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
[],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
WakuPairing: new HandshakePattern( WakuPairing: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256", "Noise_WakuPairing_25519_ChaChaPoly_SHA256",
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])], [new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
@ -116,8 +148,10 @@ export const NoiseHandshakePatterns = {
), ),
}; };
// Supported Protocol ID for PayloadV2 objects /**
// Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification * Supported Protocol ID for PayloadV2 objects
* Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
*/
export const PayloadV2ProtocolIDs: { [id: string]: number } = { export const PayloadV2ProtocolIDs: { [id: string]: number } = {
"": 0, "": 0,
Noise_K1K1_25519_ChaChaPoly_SHA256: 10, Noise_K1K1_25519_ChaChaPoly_SHA256: 10,

View File

@ -1,118 +1,19 @@
// PayloadV2 defines an object for Waku payloads with version 2 as in
// https://rfc.vac.dev/spec/35/#public-keys-serialization
// It contains a message nametag, protocol ID field, the handshake message (for Noise handshakes) and
import { concat as uint8ArrayConcat } from "uint8arrays/concat"; import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals"; import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { MessageNametag } from "./@types/handshake.js"; import { MessageNametag } from "./@types/handshake.js";
import { ChachaPolyTagLen, Curve25519KeySize, hashSHA256 } from "./crypto.js"; import { ChachaPolyTagLen, Curve25519KeySize } from "./crypto.js";
import { MessageNametagLength } from "./messagenametag.js";
import { PayloadV2ProtocolIDs } from "./patterns.js"; import { PayloadV2ProtocolIDs } from "./patterns.js";
import { NoisePublicKey } from "./publickey.js"; import { NoisePublicKey } from "./publickey.js";
import { readUIntLE, writeUIntLE } from "./utils.js"; import { readUIntLE, writeUIntLE } from "./utils.js";
export const MessageNametagLength = 16; /**
export const MessageNametagBufferSize = 50; * PayloadV2 defines an object for Waku payloads with version 2 as in
* https://rfc.vac.dev/spec/35/#public-keys-serialization
// Converts a sequence or array (arbitrary size) to a MessageNametag * It contains a message nametag, protocol ID field, the handshake message (for Noise handshakes)
export function toMessageNametag(input: Uint8Array): MessageNametag { * and the transport message
return input.subarray(0, MessageNametagLength); */
}
export class MessageNametagBuffer {
private buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
private counter = 0;
secret?: Uint8Array;
constructor() {
for (let i = 0; i < this.buffer.length; i++) {
this.buffer[i] = new Uint8Array(MessageNametagLength);
}
}
// Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n )
initNametagsBuffer(): void {
// We default the counter and buffer fields
this.counter = 0;
this.buffer = new Array<MessageNametag>(MessageNametagBufferSize);
if (this.secret) {
for (let i = 0; i < this.buffer.length; i++) {
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[i] = toMessageNametag(d);
this.counter++;
}
} else {
// We warn users if no secret is set
console.debug("The message nametags buffer has not a secret set");
}
}
pop(): MessageNametag {
// Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned
const messageNametag = new Uint8Array(this.buffer[0]);
this.delete(1);
return messageNametag;
}
// Checks if the input messageNametag is contained in the input MessageNametagBuffer
checkNametag(messageNametag: MessageNametag): boolean {
const index = this.buffer.findIndex((x) => uint8ArrayEquals(x, messageNametag));
if (index == -1) {
console.debug("Message nametag not found in buffer");
return false;
} else if (index > 0) {
console.debug(
"Message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost"
);
return false;
}
// index is 0, hence the read message tag is the next expected one
return true;
}
rotateLeft(k: number): void {
if (k < 0 || this.buffer.length == 0) {
return;
}
const idx = this.buffer.length - (k % this.buffer.length);
const a1 = this.buffer.slice(idx);
const a2 = this.buffer.slice(0, idx);
this.buffer = a1.concat(a2);
}
// Deletes the first n elements in buffer and appends n new ones
delete(n: number): void {
if (n <= 0) {
return;
}
// We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced)
n = Math.min(n, MessageNametagBufferSize);
// We update the last n values in the array if a secret is set
// Note that if the input MessageNametagBuffer is set to default, nothing is done here
if (this.secret) {
// We rotate left the array by n
this.rotateLeft(n);
for (let i = 0; i < n; i++) {
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
this.buffer[this.buffer.length - n + i] = toMessageNametag(d);
this.counter++;
}
} else {
// We warn users that no secret is set
console.debug("The message nametags buffer has no secret set");
}
}
}
export class PayloadV2 { export class PayloadV2 {
messageNametag: MessageNametag; messageNametag: MessageNametag;
protocolId: number; protocolId: number;
@ -131,6 +32,10 @@ export class PayloadV2 {
this.transportMessage = transportMessage; this.transportMessage = transportMessage;
} }
/**
* Create a copy of the PayloadV2
* @returns a copy of the PayloadV2
*/
clone(): PayloadV2 { clone(): PayloadV2 {
const r = new PayloadV2(); const r = new PayloadV2();
r.protocolId = this.protocolId; r.protocolId = this.protocolId;
@ -142,31 +47,39 @@ export class PayloadV2 {
return r; return r;
} }
equals(b: PayloadV2): boolean { /**
* Check PayloadV2 equality
* @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: PayloadV2): boolean {
let pkEquals = true; let pkEquals = true;
if (this.handshakeMessage.length != b.handshakeMessage.length) { if (this.handshakeMessage.length != other.handshakeMessage.length) {
pkEquals = false; pkEquals = false;
} }
for (let i = 0; i < this.handshakeMessage.length; i++) { for (let i = 0; i < this.handshakeMessage.length; i++) {
if (!this.handshakeMessage[i].equals(b.handshakeMessage[i])) { if (!this.handshakeMessage[i].equals(other.handshakeMessage[i])) {
pkEquals = false; pkEquals = false;
break; break;
} }
} }
return ( return (
uint8ArrayEquals(this.messageNametag, b.messageNametag) && uint8ArrayEquals(this.messageNametag, other.messageNametag) &&
this.protocolId == b.protocolId && this.protocolId == other.protocolId &&
uint8ArrayEquals(this.transportMessage, b.transportMessage) && uint8ArrayEquals(this.transportMessage, other.transportMessage) &&
pkEquals pkEquals
); );
} }
// Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/. /**
// The output serialized payload concatenates the input PayloadV2 object fields as * Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/.
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) * The output serialized payload concatenates the input PayloadV2 object fields as
// The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/ * payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
* The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
* @returns serialized payload
*/
serialize(): Uint8Array { serialize(): Uint8Array {
// We collect public keys contained in the handshake message // We collect public keys contained in the handshake message
@ -210,9 +123,11 @@ export class PayloadV2 {
return payload; return payload;
} }
// Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/. /**
// The input serialized payload concatenates the output PayloadV2 object fields as * Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
// payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) * @param payload input serialized payload
* @returns PayloadV2
*/
static deserialize(payload: Uint8Array): PayloadV2 { static deserialize(payload: Uint8Array): PayloadV2 {
// i is the read input buffer position index // i is the read input buffer position index
let i = 0; let i = 0;

View File

@ -5,19 +5,31 @@ import { bytes32 } from "./@types/basic.js";
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto.js"; import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto.js";
import { isEmptyKey } from "./noise.js"; import { isEmptyKey } from "./noise.js";
// A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad) /**
* A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
*/
export class ChaChaPolyCipherState { export class ChaChaPolyCipherState {
k: bytes32; k: bytes32;
nonce: bytes32; nonce: bytes32;
ad: Uint8Array; ad: Uint8Array;
/**
* @param k 32-byte key
* @param nonce 12 byte little-endian nonce
* @param ad associated data
*/
constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) { constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) {
this.k = k; this.k = k;
this.nonce = nonce; this.nonce = nonce;
this.ad = ad; this.ad = ad;
} }
// It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext /**
// The cipher state in not changed * Takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext.
* The cipher state in not changed
* @param plaintext data to encrypt
* @returns sealed ciphertext including authentication tag
*/
encrypt(plaintext: Uint8Array): Uint8Array { encrypt(plaintext: Uint8Array): Uint8Array {
// If plaintext is empty, we raise an error // If plaintext is empty, we raise an error
if (plaintext.length == 0) { if (plaintext.length == 0) {
@ -27,9 +39,12 @@ export class ChaChaPolyCipherState {
return chaCha20Poly1305Encrypt(plaintext, this.nonce, this.ad, this.k); return chaCha20Poly1305Encrypt(plaintext, this.nonce, this.ad, this.k);
} }
// ChaChaPoly decryption /**
// It takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext * Takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
// The cipher state is not changed * The cipher state is not changed
* @param ciphertext data to decrypt
* @returns plaintext
*/
decrypt(ciphertext: Uint8Array): Uint8Array { decrypt(ciphertext: Uint8Array): Uint8Array {
// If ciphertext is empty, we raise an error // If ciphertext is empty, we raise an error
if (ciphertext.length == 0) { if (ciphertext.length == 0) {
@ -44,30 +59,49 @@ export class ChaChaPolyCipherState {
} }
} }
// A Noise public key is a public key exchanged during Noise handshakes (no private part) /**
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization * A Noise public key is a public key exchanged during Noise handshakes (no private part)
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0) * This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
// or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1) */
// Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
export class NoisePublicKey { export class NoisePublicKey {
/**
* @param flag 1 to indicate that the public key is encrypted, 0 for unencrypted.
* Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
* @param pk contains the X coordinate of the public key, if unencrypted
* or the encryption of the X coordinate concatenated with the authorization tag, if encrypted
*/
constructor(public readonly flag: number, public readonly pk: Uint8Array) {} constructor(public readonly flag: number, public readonly pk: Uint8Array) {}
/**
* Create a copy of the NoisePublicKey
* @returns a copy of the NoisePublicKey
*/
clone(): NoisePublicKey { clone(): NoisePublicKey {
return new NoisePublicKey(this.flag, new Uint8Array(this.pk)); return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
} }
// Checks equality between two Noise public keys /**
equals(k2: NoisePublicKey): boolean { * Check NoisePublicKey equality
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk); * @param other object to compare against
* @returns true if equal, false otherwise
*/
equals(other: NoisePublicKey): boolean {
return this.flag == other.flag && uint8ArrayEquals(this.pk, other.pk);
} }
// Converts a public Elliptic Curve key to an unencrypted Noise public key /**
* Converts a public Elliptic Curve key to an unencrypted Noise public key
* @param publicKey 32-byte public key
* @returns NoisePublicKey
*/
static fromPublicKey(publicKey: bytes32): NoisePublicKey { static fromPublicKey(publicKey: bytes32): NoisePublicKey {
return new NoisePublicKey(0, publicKey); return new NoisePublicKey(0, publicKey);
} }
// Converts a Noise public key to a stream of bytes as in /**
// https://rfc.vac.dev/spec/35/#public-keys-serialization * Converts a Noise public key to a stream of bytes as in https://rfc.vac.dev/spec/35/#public-keys-serialization
* @returns Serialized NoisePublicKey
*/
serialize(): Uint8Array { serialize(): Uint8Array {
// Public key is serialized as (flag || pk) // Public key is serialized as (flag || pk)
// Note that pk contains the X coordinate of the public key if unencrypted // Note that pk contains the X coordinate of the public key if unencrypted
@ -76,8 +110,11 @@ export class NoisePublicKey {
return serializedNoisePublicKey; return serializedNoisePublicKey;
} }
// Converts a serialized Noise public key to a NoisePublicKey object as in /**
// https://rfc.vac.dev/spec/35/#public-keys-serialization * Converts a serialized Noise public key to a NoisePublicKey object as in https://rfc.vac.dev/spec/35/#public-keys-serialization
* @param serializedPK Serialized NoisePublicKey
* @returns NoisePublicKey
*/
static deserialize(serializedPK: Uint8Array): NoisePublicKey { static deserialize(serializedPK: Uint8Array): NoisePublicKey {
if (serializedPK.length == 0) throw new Error("invalid serialized key"); if (serializedPK.length == 0) throw new Error("invalid serialized key");
@ -90,30 +127,41 @@ export class NoisePublicKey {
return new NoisePublicKey(flag, pk); return new NoisePublicKey(flag, pk);
} }
static encrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey { /**
* Encrypt a NoisePublicKey using a ChaChaPolyCipherState
* @param pk NoisePublicKey to encrypt
* @param cs ChaChaPolyCipherState used to encrypt
* @returns encrypted NoisePublicKey
*/
static encrypt(pk: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
// We proceed with encryption only if // We proceed with encryption only if
// - a key is set in the cipher state // - a key is set in the cipher state
// - the public key is unencrypted // - the public key is unencrypted
if (!isEmptyKey(cs.k) && ns.flag == 0) { if (!isEmptyKey(cs.k) && pk.flag == 0) {
const encPk = cs.encrypt(ns.pk); const encPk = cs.encrypt(pk.pk);
return new NoisePublicKey(1, encPk); return new NoisePublicKey(1, encPk);
} }
// Otherwise we return the public key as it is // Otherwise we return the public key as it is
return ns.clone(); return pk.clone();
} }
// Decrypts a Noise public key using a ChaChaPoly Cipher State /**
static decrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey { * Decrypts a Noise public key using a ChaChaPoly Cipher State
* @param pk NoisePublicKey to decrypt
* @param cs ChaChaPolyCipherState used to decrypt
* @returns decrypted NoisePublicKey
*/
static decrypt(pk: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
// We proceed with decryption only if // We proceed with decryption only if
// - a key is set in the cipher state // - a key is set in the cipher state
// - the public key is encrypted // - the public key is encrypted
if (!isEmptyKey(cs.k) && ns.flag == 1) { if (!isEmptyKey(cs.k) && pk.flag == 1) {
const decrypted = cs.decrypt(ns.pk); const decrypted = cs.decrypt(pk.pk);
return new NoisePublicKey(0, decrypted); return new NoisePublicKey(0, decrypted);
} }
// Otherwise we return the public key as it is // Otherwise we return the public key as it is
return ns.clone(); return pk.clone();
} }
} }

View File

@ -2,6 +2,9 @@ import { decode, encode, fromUint8Array, toUint8Array } from "js-base64";
import { bytes32 } from "./@types/basic.js"; import { bytes32 } from "./@types/basic.js";
/**
* QR code generation
*/
export class QR { export class QR {
constructor( constructor(
public readonly applicationName: string, public readonly applicationName: string,
@ -22,14 +25,30 @@ export class QR {
return qr; return qr;
} }
/**
* Convert QR code into byte array
* @returns byte array serialization of a base64 encoded QR code
*/
toByteArray(): Uint8Array { toByteArray(): Uint8Array {
const enc = new TextEncoder(); const enc = new TextEncoder();
return enc.encode(this.toString()); return enc.encode(this.toString());
} }
// Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey) /**
static fromString(qrString: string): QR { * Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
const values = qrString.split(":"); * @param input input base64 encoded string
* @returns QR
*/
static from(input: string | Uint8Array): QR {
let qrStr: string;
if (input instanceof Uint8Array) {
const dec = new TextDecoder();
qrStr = dec.decode(input);
} else {
qrStr = input;
}
const values = qrStr.split(":");
if (values.length != 5) throw new Error("invalid qr string"); if (values.length != 5) throw new Error("invalid qr string");

View File

@ -11,8 +11,8 @@ import {
} from "./codec"; } from "./codec";
import { commitPublicKey, generateX25519KeyPair } from "./crypto"; import { commitPublicKey, generateX25519KeyPair } from "./crypto";
import { Handshake } from "./handshake"; import { Handshake } from "./handshake";
import { MessageNametagBufferSize, MessageNametagLength } from "./messagenametag";
import { NoiseHandshakePatterns } from "./patterns"; import { NoiseHandshakePatterns } from "./patterns";
import { MessageNametagBufferSize, MessageNametagLength } from "./payload";
import { NoisePublicKey } from "./publickey"; import { NoisePublicKey } from "./publickey";
import { QR } from "./qr"; import { QR } from "./qr";
@ -51,7 +51,7 @@ describe("Waku Noise Sessions", () => {
const qr = new QR(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey); const qr = new QR(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey);
// Alice deserializes the QR code // Alice deserializes the QR code
const readQR = QR.fromString(qr.toString()); const readQR = QR.from(qr.toString());
// We check if QR serialization/deserialization works // We check if QR serialization/deserialization works
expect(readQR.applicationName).to.be.equals(applicationName); expect(readQR.applicationName).to.be.equals(applicationName);