mirror of
https://github.com/waku-org/js-noise.git
synced 2025-02-23 08:28:17 +00:00
chore: add docs
This commit is contained in:
parent
6299732797
commit
e2634cc6c2
@ -14,6 +14,7 @@
|
||||
"blocksize",
|
||||
"Nametag",
|
||||
"Cipherstate",
|
||||
"cipherstates",
|
||||
"Nametags",
|
||||
"HASHLEN",
|
||||
"ciphertext",
|
||||
|
49
README.md
Normal file
49
README.md
Normal 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.
|
@ -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");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
export type bytes = Uint8Array;
|
||||
export type bytes32 = Uint8Array;
|
||||
export type bytes16 = Uint8Array;
|
||||
|
||||
|
@ -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;
|
||||
|
97
src/codec.ts
97
src/codec.ts
@ -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);
|
||||
}
|
||||
|
||||
|
103
src/crypto.ts
103
src/crypto.ts
@ -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 Diffie–Hellman 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]));
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
126
src/messagenametag.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
173
src/noise.ts
173
src/noise.ts
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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");
|
||||
|
114
src/patterns.ts
114
src/patterns.ts
@ -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,
|
||||
|
155
src/payload.ts
155
src/payload.ts
@ -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;
|
||||
|
104
src/publickey.ts
104
src/publickey.ts
@ -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();
|
||||
}
|
||||
}
|
||||
|
25
src/qr.ts
25
src/qr.ts
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user