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",
"Nametag",
"Cipherstate",
"cipherstates",
"Nametags",
"HASHLEN",
"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)();
subscriptions.delete(contentTopic);
} 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 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)
export type MessageNametag = Uint8Array;

View File

@ -10,9 +10,28 @@ const log = debug("waku:message:noise-codec");
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 {
/**
* @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) {}
async encode(message: Message): Promise<Uint8Array | undefined> {
@ -25,34 +44,21 @@ export class NoiseHandshakeEncoder implements Encoder {
const timestamp = message.timestamp ?? new Date();
return {
payload: this.hsStepResult.payload2.serialize(),
version: Version,
version: version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
};
}
}
export class NoiseHandshakeMessage extends MessageV0 implements Message {
get payloadV2(): PayloadV2 {
if (!this.payload) throw new Error("no payload available");
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;
}
}
/**
* Used in the pairing object for decoding the messages exchanged
* during the handshake process
*/
export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
/**
* @param contentTopic content topic on which the encoded WakuMessages were sent
*/
constructor(public contentTopic: string) {}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
@ -67,8 +73,8 @@ export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
proto.version = 0;
}
if (proto.version !== Version) {
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version);
if (proto.version !== version) {
log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
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 {
/**
* @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) {}
async encode(message: Message): Promise<Uint8Array | undefined> {
@ -103,14 +136,24 @@ export class NoiseSecureTransferEncoder implements Encoder {
return {
payload,
version: Version,
version: version,
contentTopic: this.contentTopic,
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> {
/**
* @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) {}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
@ -125,8 +168,8 @@ export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
proto.version = 0;
}
if (proto.version !== Version) {
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version);
if (proto.version !== version) {
log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
return Promise.resolve(undefined);
}

View File

@ -1,20 +1,32 @@
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 * as x25519 from "@stablelib/x25519";
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import type { bytes, bytes32 } from "./@types/basic.js";
import type { Hkdf } from "./@types/handshake.js";
import type { bytes32 } from "./@types/basic.js";
import type { KeyPair } from "./@types/keypair.js";
export const Curve25519KeySize = x25519.PUBLIC_KEY_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 {
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 {
if (s.length != x25519.PUBLIC_KEY_LENGTH) {
throw new Error("invalid public key length");
@ -23,20 +35,29 @@ export function intoCurve25519Key(s: Uint8Array): bytes32 {
return s;
}
export function getHKDF(ck: bytes32, ikm: Uint8Array): Hkdf {
const okm = getHKDFRaw(ck, ikm, 96);
const k1 = okm.subarray(0, 32);
const k2 = okm.subarray(32, 64);
const k3 = okm.subarray(64, 96);
return [k1, k2, k3];
}
export function getHKDFRaw(ck: bytes32, ikm: Uint8Array, numBytes: number): Uint8Array {
const hkdf = new HKDF(SHA256, ikm, ck);
return hkdf.expand(numBytes);
/**
* HKDF key derivation function using SHA256
* @param ck chaining key
* @param ikm input key material
* @param length length of each generated key
* @param numKeys number of keys to generate
* @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> {
const numBytes = length * numKeys;
const okm = new hkdf(SHA256, ikm, ck).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 {
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);
return {
@ -55,30 +81,52 @@ export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
};
}
export function generateX25519SharedKey(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
return x25519.sharedKey(privateKey, publicKey);
}
export function chaCha20Poly1305Encrypt(plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
/**
* Encrypt and authenticate data using ChaCha20-Poly1305
* @param plaintext data to encrypt
* @param nonce 12 byte little-endian nonce
* @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);
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(
ciphertext: Uint8Array,
nonce: Uint8Array,
ad: Uint8Array,
k: bytes32
): bytes | null {
): Uint8Array | null {
const ctx = new ChaCha20Poly1305(k);
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 {
try {
const derivedU8 = generateX25519SharedKey(privateKey, publicKey);
const derivedU8 = x25519.sharedKey(privateKey, publicKey);
if (derivedU8.length === 32) {
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 {
return hashSHA256(uint8ArrayConcat([publicKey, r]));
}

View File

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

View File

@ -5,10 +5,10 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic.js";
import { MessageNametag } from "./@types/handshake.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 { EmptyPreMessage, HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js";
import { MessageNametagLength } from "./payload.js";
import { HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js";
import { NoisePublicKey } from "./publickey.js";
const log = debug("waku:noise:handshake-state");
@ -101,14 +101,15 @@ export class HandshakeState {
}
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 };
}
// 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
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);
}
@ -181,7 +182,7 @@ export class HandshakeState {
// We retrieve the pre-message patterns to process, if any
// If none, there's nothing to do
if (this.handshakePattern.preMessagePatterns == EmptyPreMessage) {
if (this.handshakePattern.preMessagePatterns.length == 0) {
return;
}

View File

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

View File

@ -13,9 +13,9 @@ import {
MessageNametagError,
StepHandshakeParameters,
} from "./handshake.js";
import { MessageNametagBuffer } from "./messagenametag.js";
import { InitiatorParameters, Responder, ResponderParameters, Sender, WakuPairing } from "./pairing.js";
import {
EmptyPreMessage,
HandshakePattern,
MessageDirection,
MessagePattern,
@ -24,7 +24,6 @@ import {
PayloadV2ProtocolIDs,
PreMessagePattern,
} from "./patterns.js";
import { MessageNametagBuffer } from "./payload.js";
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js";
@ -38,7 +37,6 @@ export {
};
export { generateX25519KeyPair, generateX25519KeyPairFromSeed };
export {
EmptyPreMessage,
HandshakePattern,
MessageDirection,
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 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 { 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 {
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 {
const emptyKey = createEmptyKey();
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 {
k: bytes32;
// 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.
n: Nonce;
/**
* @param k encryption key
* @param n nonce
*/
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) {
this.k = k;
this.n = n;
}
/**
* Create a copy of the CipherState
* @returns a copy of the CipherState
*/
clone(): CipherState {
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 {
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 {
this.n.assertValue();
@ -96,8 +127,12 @@ export class CipherState {
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 {
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 {
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 {
this.k = key;
}
// Gets the key of a Cipher State
/**
* Gets the encryption key of a Cipher State
* @returns encryption key
*/
getKey(): bytes32 {
return this.k;
}
// Gets the nonce of a Cipher State
/**
* Gets the nonce of a Cipher State
* @returns Nonce
*/
getNonce(): Nonce {
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 {
// 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.
@ -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 {
cs: CipherState;
h: bytes32; // handshake hash
@ -169,15 +223,24 @@ export class SymmetricState {
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 (
this.cs.equals(b.cs) &&
uint8ArrayEquals(this.ck, b.ck) &&
uint8ArrayEquals(this.h, b.h) &&
this.hsPattern.equals(b.hsPattern)
this.cs.equals(other.cs) &&
uint8ArrayEquals(this.ck, other.ck) &&
uint8ArrayEquals(this.h, other.h) &&
this.hsPattern.equals(other.hsPattern)
);
}
/**
* Create a copy of the SymmetricState
* @returns a copy of the SymmetricState
*/
clone(): SymmetricState {
const ss = new SymmetricState(this.hsPattern);
ss.cs = this.cs.clone();
@ -186,30 +249,39 @@ export class SymmetricState {
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 {
// 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
this.cs = new CipherState(tempK);
this.ck = ck;
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 {
// 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]));
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 {
// 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
this.ck = tmpKey0;
// Updates the handshake hash value
@ -219,9 +291,14 @@ export class SymmetricState {
this.cs = new CipherState(tmpKey2);
}
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// 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)
/**
* EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
* 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 {
// The additional data
const ad = uint8ArrayConcat([this.h, extraAd]);
@ -233,8 +310,12 @@ export class SymmetricState {
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 {
// The additional data
const ad = uint8ArrayConcat([this.h, extraAd]);
@ -246,11 +327,14 @@ export class SymmetricState {
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 } {
// 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
return {
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 {
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 {
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 {
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;
// 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 {
private n: uint64;
private readonly bytes: bytes;
private readonly bytes: Uint8Array;
private readonly view: DataView;
constructor(n = MIN_NONCE) {
@ -33,7 +35,7 @@ export class Nonce {
this.view.setUint32(4, this.n, true);
}
getBytes(): bytes {
getBytes(): Uint8Array {
return this.bytes;
}

View File

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

View File

@ -16,27 +16,48 @@ import {
} from "./codec.js";
import { commitPublicKey, generateX25519KeyPair } from "./crypto.js";
import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake.js";
import { MessageNametagLength } from "./messagenametag.js";
import { NoiseHandshakePatterns } from "./patterns.js";
import { MessageNametagLength } from "./payload.js";
import { NoisePublicKey } from "./publickey.js";
import { QR } from "./qr.js";
const log = debug("waku:noise:pairing");
/**
* Sender interface that an object must implement so the pairing object can publish noise messages
*/
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>;
}
/**
* Responder interface than an object must implement so the pairing object can receive noise messages
*/
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>;
// 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)
/**
* 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)
* @param contentTopic content topic to get the next message from
*/
nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage>;
// this should stop the subscription
/**
* Stop the subscription to the content topic
* @param contentTopic
*/
stop(contentTopic: string): Promise<void>;
}
@ -46,10 +67,16 @@ function delay(ms: number): Promise<void> {
const rng = new HMACDRBG();
/**
* Initiator parameters used to setup the pairing object
*/
export class InitiatorParameters {
constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {}
}
/**
* Responder parameters used to setup the pairing object
*/
export class ResponderParameters {
constructor(
public readonly applicationName: string = "waku-noise-sessions",
@ -58,6 +85,9 @@ export class ResponderParameters {
) {}
}
/**
* Pairing object to setup a noise session
*/
export class WakuPairing {
public readonly contentTopic: string;
@ -73,12 +103,24 @@ export class WakuPairing {
private eventEmitter = new EventEmitter();
/**
* Convert a QR into a content topic
* @param qr
* @returns content topic string
*/
private static toContentTopic(qr: QR): string {
return (
"/" + 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(
private sender: Sender,
private responder: Responder,
@ -91,7 +133,7 @@ export class WakuPairing {
if (pairingParameters instanceof InitiatorParameters) {
this.initiator = true;
this.qr = QR.fromString(pairingParameters.qrCode);
this.qr = QR.from(pairingParameters.qrCode);
this.qrMessageNameTag = pairingParameters.qrMessageNameTag;
} else {
this.initiator = false;
@ -104,6 +146,7 @@ export class WakuPairing {
this.myCommittedStaticKey
);
}
// We set the contentTopic from the content topic parameters exchanged in the 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 {
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> {
return new Promise((resolve) => {
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> {
@ -301,6 +359,13 @@ export class WakuPairing {
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(
contentTopic: string,
hsResult: HandshakeResult
@ -311,6 +376,10 @@ export class WakuPairing {
return [secureEncoder, secureDecoder];
}
/**
* Get handshake result
* @returns result of a successful pairing
*/
public getHandshakeResult(): HandshakeResult {
if (!this.handshakeResult) {
throw new Error("handshake is not complete");
@ -318,14 +387,19 @@ export class WakuPairing {
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) {
throw new Error("pairing already executed. Create new pairing object");
}
this.started = true;
return new Promise((resolve, reject) => {
// Limit QR exposure to 30s
// Limit QR exposure to some timeout
const timer = setTimeout(() => {
reject(new Error("pairing has timed out"));
this.eventEmitter.emit("pairingTimeout");

View File

@ -1,6 +1,7 @@
// The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
/**
* The Noise tokens appearing in Noise (pre)message patterns
* as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
*/
export enum NoiseTokens {
e = "e",
s = "s",
@ -11,42 +12,61 @@ export enum NoiseTokens {
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 {
r = "->",
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 {
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 (
this.direction == b.direction &&
this.tokens.length === b.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index])
this.direction == other.direction &&
this.tokens.length === other.tokens.length &&
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 {
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 (
this.direction == b.direction &&
this.tokens.length === b.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index])
this.direction == other.direction &&
this.tokens.length === other.tokens.length &&
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 {
constructor(
public readonly name: string,
@ -54,25 +74,29 @@ export class HandshakePattern {
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++) {
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++) {
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 = {
K1K1: new HandshakePattern(
"Noise_K1K1_25519_ChaChaPoly_SHA256",
@ -95,16 +119,24 @@ export const NoiseHandshakePatterns = {
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
XX: new HandshakePattern("Noise_XX_25519_ChaChaPoly_SHA256", EmptyPreMessage, [
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
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", EmptyPreMessage, [
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]),
]),
XX: new HandshakePattern(
"Noise_XX_25519_ChaChaPoly_SHA256",
[],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
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(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
[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 } = {
"": 0,
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 { equals as uint8ArrayEquals } from "uint8arrays/equals";
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 { NoisePublicKey } from "./publickey.js";
import { readUIntLE, writeUIntLE } from "./utils.js";
export const MessageNametagLength = 16;
export const MessageNametagBufferSize = 50;
// Converts a sequence or array (arbitrary size) to a MessageNametag
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(): 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");
}
}
}
/**
* 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 the transport message
*/
export class PayloadV2 {
messageNametag: MessageNametag;
protocolId: number;
@ -131,6 +32,10 @@ export class PayloadV2 {
this.transportMessage = transportMessage;
}
/**
* Create a copy of the PayloadV2
* @returns a copy of the PayloadV2
*/
clone(): PayloadV2 {
const r = new PayloadV2();
r.protocolId = this.protocolId;
@ -142,31 +47,39 @@ export class PayloadV2 {
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;
if (this.handshakeMessage.length != b.handshakeMessage.length) {
if (this.handshakeMessage.length != other.handshakeMessage.length) {
pkEquals = false;
}
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;
break;
}
}
return (
uint8ArrayEquals(this.messageNametag, b.messageNametag) &&
this.protocolId == b.protocolId &&
uint8ArrayEquals(this.transportMessage, b.transportMessage) &&
uint8ArrayEquals(this.messageNametag, other.messageNametag) &&
this.protocolId == other.protocolId &&
uint8ArrayEquals(this.transportMessage, other.transportMessage) &&
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
// 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/
/**
* 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
* 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 {
// We collect public keys contained in the handshake message
@ -210,9 +123,11 @@ export class PayloadV2 {
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
// payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
/**
* Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
* @param payload input serialized payload
* @returns PayloadV2
*/
static deserialize(payload: Uint8Array): PayloadV2 {
// i is the read input buffer position index
let i = 0;

View File

@ -5,19 +5,31 @@ import { bytes32 } from "./@types/basic.js";
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto.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 {
k: bytes32;
nonce: bytes32;
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()) {
this.k = k;
this.nonce = nonce;
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 {
// If plaintext is empty, we raise an error
if (plaintext.length == 0) {
@ -27,9 +39,12 @@ export class ChaChaPolyCipherState {
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
// The cipher state is not changed
/**
* Takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
* The cipher state is not changed
* @param ciphertext data to decrypt
* @returns plaintext
*/
decrypt(ciphertext: Uint8Array): Uint8Array {
// If ciphertext is empty, we raise an error
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
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
// 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
/**
* 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
*/
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) {}
/**
* Create a copy of the NoisePublicKey
* @returns a copy of the NoisePublicKey
*/
clone(): NoisePublicKey {
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
}
// Checks equality between two Noise public keys
equals(k2: NoisePublicKey): boolean {
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk);
/**
* Check NoisePublicKey equality
* @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 {
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 {
// Public key is serialized as (flag || pk)
// Note that pk contains the X coordinate of the public key if unencrypted
@ -76,8 +110,11 @@ export class NoisePublicKey {
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 {
if (serializedPK.length == 0) throw new Error("invalid serialized key");
@ -90,30 +127,41 @@ export class NoisePublicKey {
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
// - a key is set in the cipher state
// - the public key is unencrypted
if (!isEmptyKey(cs.k) && ns.flag == 0) {
const encPk = cs.encrypt(ns.pk);
if (!isEmptyKey(cs.k) && pk.flag == 0) {
const encPk = cs.encrypt(pk.pk);
return new NoisePublicKey(1, encPk);
}
// 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
// - a key is set in the cipher state
// - the public key is encrypted
if (!isEmptyKey(cs.k) && ns.flag == 1) {
const decrypted = cs.decrypt(ns.pk);
if (!isEmptyKey(cs.k) && pk.flag == 1) {
const decrypted = cs.decrypt(pk.pk);
return new NoisePublicKey(0, decrypted);
}
// 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";
/**
* QR code generation
*/
export class QR {
constructor(
public readonly applicationName: string,
@ -22,14 +25,30 @@ export class QR {
return qr;
}
/**
* Convert QR code into byte array
* @returns byte array serialization of a base64 encoded QR code
*/
toByteArray(): Uint8Array {
const enc = new TextEncoder();
return enc.encode(this.toString());
}
// Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
static fromString(qrString: string): QR {
const values = qrString.split(":");
/**
* Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
* @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");

View File

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