mirror of
https://github.com/logos-messaging/js-noise.git
synced 2026-01-07 16:13:08 +00:00
chore: add docs
This commit is contained in:
parent
6299732797
commit
e2634cc6c2
@ -14,6 +14,7 @@
|
|||||||
"blocksize",
|
"blocksize",
|
||||||
"Nametag",
|
"Nametag",
|
||||||
"Cipherstate",
|
"Cipherstate",
|
||||||
|
"cipherstates",
|
||||||
"Nametags",
|
"Nametags",
|
||||||
"HASHLEN",
|
"HASHLEN",
|
||||||
"ciphertext",
|
"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)();
|
await subscriptions.get(contentTopic)();
|
||||||
subscriptions.delete(contentTopic);
|
subscriptions.delete(contentTopic);
|
||||||
} else {
|
} 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 bytes32 = Uint8Array;
|
||||||
export type bytes16 = 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)
|
// a transport message (for Noise handshakes and ChaChaPoly encryptions)
|
||||||
export type MessageNametag = Uint8Array;
|
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);
|
const OneMillion = BigInt(1_000_000);
|
||||||
|
|
||||||
export const Version = 2;
|
// WakuMessage version for noise protocol
|
||||||
|
const version = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used internally in the pairing object to represent a handshake message
|
||||||
|
*/
|
||||||
|
export class NoiseHandshakeMessage extends MessageV0 implements Message {
|
||||||
|
get payloadV2(): PayloadV2 {
|
||||||
|
if (!this.payload) throw new Error("no payload available");
|
||||||
|
return PayloadV2.deserialize(this.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in the pairing object for encoding the messages exchanged
|
||||||
|
* during the handshake process
|
||||||
|
*/
|
||||||
export class NoiseHandshakeEncoder implements Encoder {
|
export class NoiseHandshakeEncoder implements Encoder {
|
||||||
|
/**
|
||||||
|
* @param contentTopic content topic on which the encoded WakuMessages will be sent
|
||||||
|
* @param hsStepResult the result of a step executed while performing the handshake process
|
||||||
|
*/
|
||||||
constructor(public contentTopic: string, private hsStepResult: HandshakeStepResult) {}
|
constructor(public contentTopic: string, private hsStepResult: HandshakeStepResult) {}
|
||||||
|
|
||||||
async encode(message: Message): Promise<Uint8Array | undefined> {
|
async encode(message: Message): Promise<Uint8Array | undefined> {
|
||||||
@ -25,34 +44,21 @@ export class NoiseHandshakeEncoder implements Encoder {
|
|||||||
const timestamp = message.timestamp ?? new Date();
|
const timestamp = message.timestamp ?? new Date();
|
||||||
return {
|
return {
|
||||||
payload: this.hsStepResult.payload2.serialize(),
|
payload: this.hsStepResult.payload2.serialize(),
|
||||||
version: Version,
|
version: version,
|
||||||
contentTopic: this.contentTopic,
|
contentTopic: this.contentTopic,
|
||||||
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NoiseHandshakeMessage extends MessageV0 implements Message {
|
/**
|
||||||
get payloadV2(): PayloadV2 {
|
* Used in the pairing object for decoding the messages exchanged
|
||||||
if (!this.payload) throw new Error("no payload available");
|
* during the handshake process
|
||||||
return PayloadV2.deserialize(this.payload);
|
*/
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NoiseSecureMessage extends MessageV0 implements Message {
|
|
||||||
private readonly _decodedPayload: Uint8Array;
|
|
||||||
|
|
||||||
constructor(proto: proto_message.WakuMessage, decodedPayload: Uint8Array) {
|
|
||||||
super(proto);
|
|
||||||
this._decodedPayload = decodedPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
get payload(): Uint8Array {
|
|
||||||
return this._decodedPayload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
|
export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
|
||||||
|
/**
|
||||||
|
* @param contentTopic content topic on which the encoded WakuMessages were sent
|
||||||
|
*/
|
||||||
constructor(public contentTopic: string) {}
|
constructor(public contentTopic: string) {}
|
||||||
|
|
||||||
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
|
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
|
||||||
@ -67,8 +73,8 @@ export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
|
|||||||
proto.version = 0;
|
proto.version = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proto.version !== Version) {
|
if (proto.version !== version) {
|
||||||
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version);
|
log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +87,34 @@ export class NoiseHandshakeDecoder implements Decoder<NoiseHandshakeMessage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a secure message. These are messages that are transmitted
|
||||||
|
* after a successful handshake is performed.
|
||||||
|
*/
|
||||||
|
export class NoiseSecureMessage extends MessageV0 implements Message {
|
||||||
|
private readonly _decodedPayload: Uint8Array;
|
||||||
|
|
||||||
|
constructor(proto: proto_message.WakuMessage, decodedPayload: Uint8Array) {
|
||||||
|
super(proto);
|
||||||
|
this._decodedPayload = decodedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
get payload(): Uint8Array {
|
||||||
|
return this._decodedPayload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* js-waku encoder for secure messages. After a handshake is successful, a
|
||||||
|
* codec for encoding messages is generated. The messages encoded with this
|
||||||
|
* codec will be encrypted with the cipherstates and message nametags that were
|
||||||
|
* created after a handshake is complete
|
||||||
|
*/
|
||||||
export class NoiseSecureTransferEncoder implements Encoder {
|
export class NoiseSecureTransferEncoder implements Encoder {
|
||||||
|
/**
|
||||||
|
* @param contentTopic content topic on which the encoded WakuMessages were sent
|
||||||
|
* @param hsResult handshake result obtained after the handshake is successful
|
||||||
|
*/
|
||||||
constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
|
constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
|
||||||
|
|
||||||
async encode(message: Message): Promise<Uint8Array | undefined> {
|
async encode(message: Message): Promise<Uint8Array | undefined> {
|
||||||
@ -103,14 +136,24 @@ export class NoiseSecureTransferEncoder implements Encoder {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
payload,
|
payload,
|
||||||
version: Version,
|
version: version,
|
||||||
contentTopic: this.contentTopic,
|
contentTopic: this.contentTopic,
|
||||||
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* js-waku decoder for secure messages. After a handshake is successful, a codec
|
||||||
|
* for decoding messages is generated. This decoder will attempt to decrypt
|
||||||
|
* messages with the cipherstates and message nametags that were created after a
|
||||||
|
* handshake is complete
|
||||||
|
*/
|
||||||
export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
|
export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
|
||||||
|
/**
|
||||||
|
* @param contentTopic content topic on which the encoded WakuMessages were sent
|
||||||
|
* @param hsResult handshake result obtained after the handshake is successful
|
||||||
|
*/
|
||||||
constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
|
constructor(public contentTopic: string, private hsResult: HandshakeResult) {}
|
||||||
|
|
||||||
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
|
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
|
||||||
@ -125,8 +168,8 @@ export class NoiseSecureTransferDecoder implements Decoder<NoiseSecureMessage> {
|
|||||||
proto.version = 0;
|
proto.version = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proto.version !== Version) {
|
if (proto.version !== version) {
|
||||||
log("Failed to decode due to incorrect version, expected:", Version, ", actual:", proto.version);
|
log("Failed to decode due to incorrect version, expected:", version, ", actual:", proto.version);
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
src/crypto.ts
103
src/crypto.ts
@ -1,20 +1,32 @@
|
|||||||
import { ChaCha20Poly1305, TAG_LENGTH } from "@stablelib/chacha20poly1305";
|
import { ChaCha20Poly1305, TAG_LENGTH } from "@stablelib/chacha20poly1305";
|
||||||
import { HKDF } from "@stablelib/hkdf";
|
import { HKDF as hkdf } from "@stablelib/hkdf";
|
||||||
import { hash, SHA256 } from "@stablelib/sha256";
|
import { hash, SHA256 } from "@stablelib/sha256";
|
||||||
import * as x25519 from "@stablelib/x25519";
|
import * as x25519 from "@stablelib/x25519";
|
||||||
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
||||||
|
|
||||||
import type { bytes, bytes32 } from "./@types/basic.js";
|
import type { bytes32 } from "./@types/basic.js";
|
||||||
import type { Hkdf } from "./@types/handshake.js";
|
|
||||||
import type { KeyPair } from "./@types/keypair.js";
|
import type { KeyPair } from "./@types/keypair.js";
|
||||||
|
|
||||||
export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH;
|
export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH;
|
||||||
|
|
||||||
export const ChachaPolyTagLen = TAG_LENGTH;
|
export const ChachaPolyTagLen = TAG_LENGTH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate hash using SHA2-256
|
||||||
|
* @param data data to hash
|
||||||
|
* @returns hash digest
|
||||||
|
*/
|
||||||
export function hashSHA256(data: Uint8Array): Uint8Array {
|
export function hashSHA256(data: Uint8Array): Uint8Array {
|
||||||
return hash(data);
|
return hash(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an Uint8Array into a 32-byte value. If the input data length is different
|
||||||
|
* from 32, throw an error. This is used mostly as a validation function to ensure
|
||||||
|
* that an Uint8Array represents a valid x25519 key
|
||||||
|
* @param s input data
|
||||||
|
* @return 32-byte key
|
||||||
|
*/
|
||||||
export function intoCurve25519Key(s: Uint8Array): bytes32 {
|
export function intoCurve25519Key(s: Uint8Array): bytes32 {
|
||||||
if (s.length != x25519.PUBLIC_KEY_LENGTH) {
|
if (s.length != x25519.PUBLIC_KEY_LENGTH) {
|
||||||
throw new Error("invalid public key length");
|
throw new Error("invalid public key length");
|
||||||
@ -23,20 +35,29 @@ export function intoCurve25519Key(s: Uint8Array): bytes32 {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHKDF(ck: bytes32, ikm: Uint8Array): Hkdf {
|
/**
|
||||||
const okm = getHKDFRaw(ck, ikm, 96);
|
* HKDF key derivation function using SHA256
|
||||||
const k1 = okm.subarray(0, 32);
|
* @param ck chaining key
|
||||||
const k2 = okm.subarray(32, 64);
|
* @param ikm input key material
|
||||||
const k3 = okm.subarray(64, 96);
|
* @param length length of each generated key
|
||||||
|
* @param numKeys number of keys to generate
|
||||||
return [k1, k2, k3];
|
* @returns array of `numValues` length containing Uint8Array keys of a given byte `length`
|
||||||
}
|
*/
|
||||||
|
export function HKDF(ck: bytes32, ikm: Uint8Array, length: number, numKeys: number): Array<Uint8Array> {
|
||||||
export function getHKDFRaw(ck: bytes32, ikm: Uint8Array, numBytes: number): Uint8Array {
|
const numBytes = length * numKeys;
|
||||||
const hkdf = new HKDF(SHA256, ikm, ck);
|
const okm = new hkdf(SHA256, ikm, ck).expand(numBytes);
|
||||||
return hkdf.expand(numBytes);
|
const result = [];
|
||||||
|
for (let i = 0; i < numBytes; i += length) {
|
||||||
|
const k = okm.subarray(i, i + length);
|
||||||
|
result.push(k);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random keypair
|
||||||
|
* @returns Keypair
|
||||||
|
*/
|
||||||
export function generateX25519KeyPair(): KeyPair {
|
export function generateX25519KeyPair(): KeyPair {
|
||||||
const keypair = x25519.generateKeyPair();
|
const keypair = x25519.generateKeyPair();
|
||||||
|
|
||||||
@ -46,7 +67,12 @@ export function generateX25519KeyPair(): KeyPair {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
|
/**
|
||||||
|
* Generate x25519 keypair using an input seed
|
||||||
|
* @param seed 32-byte secret
|
||||||
|
* @returns Keypair
|
||||||
|
*/
|
||||||
|
export function generateX25519KeyPairFromSeed(seed: bytes32): KeyPair {
|
||||||
const keypair = x25519.generateKeyPairFromSeed(seed);
|
const keypair = x25519.generateKeyPairFromSeed(seed);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -55,30 +81,52 @@ export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateX25519SharedKey(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
|
/**
|
||||||
return x25519.sharedKey(privateKey, publicKey);
|
* Encrypt and authenticate data using ChaCha20-Poly1305
|
||||||
}
|
* @param plaintext data to encrypt
|
||||||
|
* @param nonce 12 byte little-endian nonce
|
||||||
export function chaCha20Poly1305Encrypt(plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
|
* @param ad associated data
|
||||||
|
* @param k 32-byte key
|
||||||
|
* @returns sealed ciphertext including authentication tag
|
||||||
|
*/
|
||||||
|
export function chaCha20Poly1305Encrypt(
|
||||||
|
plaintext: Uint8Array,
|
||||||
|
nonce: Uint8Array,
|
||||||
|
ad: Uint8Array,
|
||||||
|
k: bytes32
|
||||||
|
): Uint8Array {
|
||||||
const ctx = new ChaCha20Poly1305(k);
|
const ctx = new ChaCha20Poly1305(k);
|
||||||
|
|
||||||
return ctx.seal(nonce, plaintext, ad);
|
return ctx.seal(nonce, plaintext, ad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate and decrypt data using ChaCha20-Poly1305
|
||||||
|
* @param ciphertext data to decrypt
|
||||||
|
* @param nonce 12 byte little-endian nonce
|
||||||
|
* @param ad associated data
|
||||||
|
* @param k 32-byte key
|
||||||
|
* @returns plaintext if decryption was successful, `null` otherwise
|
||||||
|
*/
|
||||||
export function chaCha20Poly1305Decrypt(
|
export function chaCha20Poly1305Decrypt(
|
||||||
ciphertext: Uint8Array,
|
ciphertext: Uint8Array,
|
||||||
nonce: Uint8Array,
|
nonce: Uint8Array,
|
||||||
ad: Uint8Array,
|
ad: Uint8Array,
|
||||||
k: bytes32
|
k: bytes32
|
||||||
): bytes | null {
|
): Uint8Array | null {
|
||||||
const ctx = new ChaCha20Poly1305(k);
|
const ctx = new ChaCha20Poly1305(k);
|
||||||
|
|
||||||
return ctx.open(nonce, ciphertext, ad);
|
return ctx.open(nonce, ciphertext, ad);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a 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 {
|
export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
|
||||||
try {
|
try {
|
||||||
const derivedU8 = generateX25519SharedKey(privateKey, publicKey);
|
const derivedU8 = x25519.sharedKey(privateKey, publicKey);
|
||||||
|
|
||||||
if (derivedU8.length === 32) {
|
if (derivedU8.length === 32) {
|
||||||
return derivedU8;
|
return derivedU8;
|
||||||
@ -91,7 +139,12 @@ export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commits a public key pk for randomness r as H(pk || s)
|
/**
|
||||||
|
* Generates a random static key commitment using a public key pk for randomness r as H(pk || s)
|
||||||
|
* @param publicKey x25519 public key
|
||||||
|
* @param r random fixed-length value
|
||||||
|
* @returns 32 byte hash
|
||||||
|
*/
|
||||||
export function commitPublicKey(publicKey: bytes32, r: Uint8Array): bytes32 {
|
export function commitPublicKey(publicKey: bytes32, r: Uint8Array): bytes32 {
|
||||||
return hashSHA256(uint8ArrayConcat([publicKey, r]));
|
return hashSHA256(uint8ArrayConcat([publicKey, r]));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
|||||||
|
|
||||||
import { bytes32 } from "./@types/basic";
|
import { bytes32 } from "./@types/basic";
|
||||||
import { KeyPair } from "./@types/keypair";
|
import { KeyPair } from "./@types/keypair";
|
||||||
import { getHKDFRaw } from "./crypto.js";
|
import { HKDF } from "./crypto";
|
||||||
import { HandshakeState, NoisePaddingBlockSize } from "./handshake_state.js";
|
import { HandshakeState, NoisePaddingBlockSize } from "./handshake_state.js";
|
||||||
|
import { MessageNametagBuffer, toMessageNametag } from "./messagenametag";
|
||||||
import { CipherState } from "./noise.js";
|
import { CipherState } from "./noise.js";
|
||||||
import { HandshakePattern, PayloadV2ProtocolIDs } from "./patterns.js";
|
import { HandshakePattern, PayloadV2ProtocolIDs } from "./patterns.js";
|
||||||
import { MessageNametagBuffer, PayloadV2, toMessageNametag } from "./payload.js";
|
import { PayloadV2 } from "./payload.js";
|
||||||
import { NoisePublicKey } from "./publickey.js";
|
import { NoisePublicKey } from "./publickey.js";
|
||||||
|
|
||||||
// Noise state machine
|
// Noise state machine
|
||||||
@ -172,7 +173,7 @@ export class Handshake {
|
|||||||
|
|
||||||
// Generates an 8 decimal digits authorization code using HKDF and the handshake state
|
// Generates an 8 decimal digits authorization code using HKDF and the handshake state
|
||||||
genAuthcode(): string {
|
genAuthcode(): string {
|
||||||
const output0 = getHKDFRaw(this.hs.ss.h, new Uint8Array(), 8);
|
const [output0] = HKDF(this.hs.ss.h, new Uint8Array(), 8, 1);
|
||||||
const bn = new BN(output0);
|
const bn = new BN(output0);
|
||||||
const code = bn.mod(new BN(100_000_000)).toString().padStart(8, "0");
|
const code = bn.mod(new BN(100_000_000)).toString().padStart(8, "0");
|
||||||
return code.toString();
|
return code.toString();
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
|||||||
import { bytes32 } from "./@types/basic.js";
|
import { bytes32 } from "./@types/basic.js";
|
||||||
import { MessageNametag } from "./@types/handshake.js";
|
import { MessageNametag } from "./@types/handshake.js";
|
||||||
import type { KeyPair } from "./@types/keypair.js";
|
import type { KeyPair } from "./@types/keypair.js";
|
||||||
import { Curve25519KeySize, dh, generateX25519KeyPair, getHKDF, intoCurve25519Key } from "./crypto.js";
|
import { Curve25519KeySize, dh, generateX25519KeyPair, HKDF, intoCurve25519Key } from "./crypto.js";
|
||||||
|
import { MessageNametagLength } from "./messagenametag";
|
||||||
import { SymmetricState } from "./noise.js";
|
import { SymmetricState } from "./noise.js";
|
||||||
import { EmptyPreMessage, HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js";
|
import { HandshakePattern, MessageDirection, NoiseTokens, PreMessagePattern } from "./patterns.js";
|
||||||
import { MessageNametagLength } from "./payload.js";
|
|
||||||
import { NoisePublicKey } from "./publickey.js";
|
import { NoisePublicKey } from "./publickey.js";
|
||||||
|
|
||||||
const log = debug("waku:noise:handshake-state");
|
const log = debug("waku:noise:handshake-state");
|
||||||
@ -101,14 +101,15 @@ export class HandshakeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } {
|
genMessageNametagSecrets(): { nms1: Uint8Array; nms2: Uint8Array } {
|
||||||
const [nms1, nms2] = getHKDF(this.ss.h, new Uint8Array());
|
const [nms1, nms2] = HKDF(this.ss.h, new Uint8Array(), 2, 32);
|
||||||
return { nms1, nms2 };
|
return { nms1, nms2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag
|
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag
|
||||||
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
|
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
|
||||||
toMessageNametag(): MessageNametag {
|
toMessageNametag(): MessageNametag {
|
||||||
const [output] = getHKDF(this.ss.h, new Uint8Array());
|
console.log("HELLO!");
|
||||||
|
const [output] = HKDF(this.ss.h, new Uint8Array(), 32, 1);
|
||||||
return output.subarray(0, MessageNametagLength);
|
return output.subarray(0, MessageNametagLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +182,7 @@ export class HandshakeState {
|
|||||||
|
|
||||||
// We retrieve the pre-message patterns to process, if any
|
// We retrieve the pre-message patterns to process, if any
|
||||||
// If none, there's nothing to do
|
// If none, there's nothing to do
|
||||||
if (this.handshakePattern.preMessagePatterns == EmptyPreMessage) {
|
if (this.handshakePattern.preMessagePatterns.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,11 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
|||||||
|
|
||||||
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
|
import { chaCha20Poly1305Encrypt, dh, generateX25519KeyPair } from "./crypto";
|
||||||
import { Handshake, HandshakeStepResult } from "./handshake";
|
import { Handshake, HandshakeStepResult } from "./handshake";
|
||||||
|
import { MessageNametagBuffer, MessageNametagLength } from "./messagenametag";
|
||||||
import { CipherState, createEmptyKey, SymmetricState } from "./noise";
|
import { CipherState, createEmptyKey, SymmetricState } from "./noise";
|
||||||
import { MAX_NONCE, Nonce } from "./nonce";
|
import { MAX_NONCE, Nonce } from "./nonce";
|
||||||
import { NoiseHandshakePatterns } from "./patterns";
|
import { NoiseHandshakePatterns } from "./patterns";
|
||||||
import { MessageNametagBuffer, MessageNametagLength, PayloadV2 } from "./payload";
|
import { PayloadV2 } from "./payload";
|
||||||
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey";
|
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey";
|
||||||
|
|
||||||
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
|
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
|
||||||
|
|||||||
@ -13,9 +13,9 @@ import {
|
|||||||
MessageNametagError,
|
MessageNametagError,
|
||||||
StepHandshakeParameters,
|
StepHandshakeParameters,
|
||||||
} from "./handshake.js";
|
} from "./handshake.js";
|
||||||
|
import { MessageNametagBuffer } from "./messagenametag.js";
|
||||||
import { InitiatorParameters, Responder, ResponderParameters, Sender, WakuPairing } from "./pairing.js";
|
import { InitiatorParameters, Responder, ResponderParameters, Sender, WakuPairing } from "./pairing.js";
|
||||||
import {
|
import {
|
||||||
EmptyPreMessage,
|
|
||||||
HandshakePattern,
|
HandshakePattern,
|
||||||
MessageDirection,
|
MessageDirection,
|
||||||
MessagePattern,
|
MessagePattern,
|
||||||
@ -24,7 +24,6 @@ import {
|
|||||||
PayloadV2ProtocolIDs,
|
PayloadV2ProtocolIDs,
|
||||||
PreMessagePattern,
|
PreMessagePattern,
|
||||||
} from "./patterns.js";
|
} from "./patterns.js";
|
||||||
import { MessageNametagBuffer } from "./payload.js";
|
|
||||||
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js";
|
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey.js";
|
||||||
import { QR } from "./qr.js";
|
import { QR } from "./qr.js";
|
||||||
|
|
||||||
@ -38,7 +37,6 @@ export {
|
|||||||
};
|
};
|
||||||
export { generateX25519KeyPair, generateX25519KeyPairFromSeed };
|
export { generateX25519KeyPair, generateX25519KeyPairFromSeed };
|
||||||
export {
|
export {
|
||||||
EmptyPreMessage,
|
|
||||||
HandshakePattern,
|
HandshakePattern,
|
||||||
MessageDirection,
|
MessageDirection,
|
||||||
MessagePattern,
|
MessagePattern,
|
||||||
|
|||||||
126
src/messagenametag.ts
Normal file
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 { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||||
|
|
||||||
import type { bytes32 } from "./@types/basic.js";
|
import type { bytes32 } from "./@types/basic.js";
|
||||||
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, getHKDF, hashSHA256 } from "./crypto.js";
|
import { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, hashSHA256, HKDF } from "./crypto.js";
|
||||||
import { Nonce } from "./nonce.js";
|
import { Nonce } from "./nonce.js";
|
||||||
import { HandshakePattern } from "./patterns.js";
|
import { HandshakePattern } from "./patterns.js";
|
||||||
|
|
||||||
@ -38,43 +38,74 @@ const log = debug("waku:noise:handshake-state");
|
|||||||
#################################
|
#################################
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create empty chaining key
|
||||||
|
* @returns 32-byte empty key
|
||||||
|
*/
|
||||||
export function createEmptyKey(): bytes32 {
|
export function createEmptyKey(): bytes32 {
|
||||||
return new Uint8Array(32);
|
return new Uint8Array(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a 32-byte key is empty
|
||||||
|
* @param k key to verify
|
||||||
|
* @returns true if empty, false otherwise
|
||||||
|
*/
|
||||||
export function isEmptyKey(k: bytes32): boolean {
|
export function isEmptyKey(k: bytes32): boolean {
|
||||||
const emptyKey = createEmptyKey();
|
const emptyKey = createEmptyKey();
|
||||||
return uint8ArrayEquals(emptyKey, k);
|
return uint8ArrayEquals(emptyKey, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
|
/**
|
||||||
// Contains an encryption key k and a nonce n (used in Noise as a counter)
|
* The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
|
||||||
|
* Contains an encryption key k and a nonce n (used in Noise as a counter)
|
||||||
|
*/
|
||||||
export class CipherState {
|
export class CipherState {
|
||||||
k: bytes32;
|
k: bytes32;
|
||||||
// For performance reasons, the nonce is represented as a Nonce object
|
// For performance reasons, the nonce is represented as a Nonce object
|
||||||
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
|
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
|
||||||
n: Nonce;
|
n: Nonce;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param k encryption key
|
||||||
|
* @param n nonce
|
||||||
|
*/
|
||||||
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) {
|
constructor(k: bytes32 = createEmptyKey(), n = new Nonce()) {
|
||||||
this.k = k;
|
this.k = k;
|
||||||
this.n = n;
|
this.n = n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the CipherState
|
||||||
|
* @returns a copy of the CipherState
|
||||||
|
*/
|
||||||
clone(): CipherState {
|
clone(): CipherState {
|
||||||
return new CipherState(new Uint8Array(this.k), new Nonce(this.n.getUint64()));
|
return new CipherState(new Uint8Array(this.k), new Nonce(this.n.getUint64()));
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(b: CipherState): boolean {
|
/**
|
||||||
return uint8ArrayEquals(this.k, b.getKey()) && this.n.getUint64() == b.getNonce().getUint64();
|
* Check CipherState equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: CipherState): boolean {
|
||||||
|
return uint8ArrayEquals(this.k, other.getKey()) && this.n.getUint64() == other.getNonce().getUint64();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if a Cipher State has an encryption key set
|
/**
|
||||||
|
* Checks if a Cipher State has an encryption key set
|
||||||
|
* @returns true if a key is set, false otherwise`
|
||||||
|
*/
|
||||||
protected hasKey(): boolean {
|
protected hasKey(): boolean {
|
||||||
return !isEmptyKey(this.k);
|
return !isEmptyKey(this.k);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypts a plaintext using key material in a Noise Cipher State
|
/**
|
||||||
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
* Encrypts a plaintext using key material in a Noise Cipher State
|
||||||
|
* The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||||
|
* @param ad associated data
|
||||||
|
* @param plaintext data to encrypt
|
||||||
|
*/
|
||||||
encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
||||||
this.n.assertValue();
|
this.n.assertValue();
|
||||||
|
|
||||||
@ -96,8 +127,12 @@ export class CipherState {
|
|||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypts a ciphertext using key material in a Noise Cipher State
|
/**
|
||||||
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
* Decrypts a ciphertext using key material in a Noise Cipher State
|
||||||
|
* The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||||
|
* @param ad associated data
|
||||||
|
* @param ciphertext data to decrypt
|
||||||
|
*/
|
||||||
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
|
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
|
||||||
this.n.assertValue();
|
this.n.assertValue();
|
||||||
|
|
||||||
@ -119,27 +154,44 @@ export class CipherState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the nonce of a Cipher State
|
/**
|
||||||
|
* Sets the nonce of a Cipher State
|
||||||
|
* @param nonce Nonce
|
||||||
|
*/
|
||||||
setNonce(nonce: Nonce): void {
|
setNonce(nonce: Nonce): void {
|
||||||
this.n = nonce;
|
this.n = nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the key of a Cipher State
|
/**
|
||||||
|
* Sets the key of a Cipher State
|
||||||
|
* @param key set the cipherstate encryption key
|
||||||
|
*/
|
||||||
setCipherStateKey(key: bytes32): void {
|
setCipherStateKey(key: bytes32): void {
|
||||||
this.k = key;
|
this.k = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the key of a Cipher State
|
/**
|
||||||
|
* Gets the encryption key of a Cipher State
|
||||||
|
* @returns encryption key
|
||||||
|
*/
|
||||||
getKey(): bytes32 {
|
getKey(): bytes32 {
|
||||||
return this.k;
|
return this.k;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the nonce of a Cipher State
|
/**
|
||||||
|
* Gets the nonce of a Cipher State
|
||||||
|
* @returns Nonce
|
||||||
|
*/
|
||||||
getNonce(): Nonce {
|
getNonce(): Nonce {
|
||||||
return this.n;
|
return this.n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash protocol name
|
||||||
|
* @param name name of the noise handshake pattern to hash
|
||||||
|
* @returns sha256 digest of the protocol name
|
||||||
|
*/
|
||||||
function hashProtocol(name: string): Uint8Array {
|
function hashProtocol(name: string): Uint8Array {
|
||||||
// If protocol_name is less than or equal to HASHLEN bytes in length,
|
// If protocol_name is less than or equal to HASHLEN bytes in length,
|
||||||
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
|
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
|
||||||
@ -155,8 +207,10 @@ function hashProtocol(name: string): Uint8Array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h
|
* The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Contains a Cipher State cs, the chaining key ck and the handshake hash value h
|
||||||
|
*/
|
||||||
export class SymmetricState {
|
export class SymmetricState {
|
||||||
cs: CipherState;
|
cs: CipherState;
|
||||||
h: bytes32; // handshake hash
|
h: bytes32; // handshake hash
|
||||||
@ -169,15 +223,24 @@ export class SymmetricState {
|
|||||||
this.hsPattern = hsPattern;
|
this.hsPattern = hsPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(b: SymmetricState): boolean {
|
/**
|
||||||
|
* Check CipherState equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: SymmetricState): boolean {
|
||||||
return (
|
return (
|
||||||
this.cs.equals(b.cs) &&
|
this.cs.equals(other.cs) &&
|
||||||
uint8ArrayEquals(this.ck, b.ck) &&
|
uint8ArrayEquals(this.ck, other.ck) &&
|
||||||
uint8ArrayEquals(this.h, b.h) &&
|
uint8ArrayEquals(this.h, other.h) &&
|
||||||
this.hsPattern.equals(b.hsPattern)
|
this.hsPattern.equals(other.hsPattern)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the SymmetricState
|
||||||
|
* @returns a copy of the SymmetricState
|
||||||
|
*/
|
||||||
clone(): SymmetricState {
|
clone(): SymmetricState {
|
||||||
const ss = new SymmetricState(this.hsPattern);
|
const ss = new SymmetricState(this.hsPattern);
|
||||||
ss.cs = this.cs.clone();
|
ss.cs = this.cs.clone();
|
||||||
@ -186,30 +249,39 @@ export class SymmetricState {
|
|||||||
return ss;
|
return ss;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Updates a Symmetric state chaining key and symmetric state
|
* MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Updates a Symmetric state chaining key and symmetric state
|
||||||
|
* @param inputKeyMaterial
|
||||||
|
*/
|
||||||
mixKey(inputKeyMaterial: Uint8Array): void {
|
mixKey(inputKeyMaterial: Uint8Array): void {
|
||||||
// We derive two keys using HKDF
|
// We derive two keys using HKDF
|
||||||
const [ck, tempK] = getHKDF(this.ck, inputKeyMaterial);
|
const [ck, tempK] = HKDF(this.ck, inputKeyMaterial, 32, 2);
|
||||||
// We update ck and the Cipher state's key k using the output of HDKF
|
// We update ck and the Cipher state's key k using the output of HDKF
|
||||||
this.cs = new CipherState(tempK);
|
this.cs = new CipherState(tempK);
|
||||||
this.ck = ck;
|
this.ck = ck;
|
||||||
log("mixKey", this.ck, this.cs.k);
|
log("mixKey", this.ck, this.cs.k);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Hashes data into a Symmetric State's handshake hash value h
|
* MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Hashes data into a Symmetric State's handshake hash value h
|
||||||
|
* @param data input data to hash into h
|
||||||
|
*/
|
||||||
mixHash(data: Uint8Array): void {
|
mixHash(data: Uint8Array): void {
|
||||||
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
|
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
|
||||||
this.h = hashSHA256(uint8ArrayConcat([this.h, data]));
|
this.h = hashSHA256(uint8ArrayConcat([this.h, data]));
|
||||||
log("mixHash", this.h);
|
log("mixHash", this.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Combines MixKey and MixHash
|
* mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Combines MixKey and MixHash
|
||||||
|
* @param inputKeyMaterial
|
||||||
|
*/
|
||||||
mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
|
mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
|
||||||
// Derives 3 keys using HKDF, the chaining key and the input key material
|
// Derives 3 keys using HKDF, the chaining key and the input key material
|
||||||
const [tmpKey0, tmpKey1, tmpKey2] = getHKDF(this.ck, inputKeyMaterial);
|
const [tmpKey0, tmpKey1, tmpKey2] = HKDF(this.ck, inputKeyMaterial, 32, 3);
|
||||||
// Sets the chaining key
|
// Sets the chaining key
|
||||||
this.ck = tmpKey0;
|
this.ck = tmpKey0;
|
||||||
// Updates the handshake hash value
|
// Updates the handshake hash value
|
||||||
@ -219,9 +291,14 @@ export class SymmetricState {
|
|||||||
this.cs = new CipherState(tmpKey2);
|
this.cs = new CipherState(tmpKey2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Combines encryptWithAd and mixHash
|
* EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
// Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
|
* Combines encryptWithAd and mixHash
|
||||||
|
* Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad
|
||||||
|
* specified by Noise (can be used to authenticate messageNametag)
|
||||||
|
* @param plaintext data to encrypt
|
||||||
|
* @param extraAd extra additional data
|
||||||
|
*/
|
||||||
encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
|
encryptAndHash(plaintext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
|
||||||
// The additional data
|
// The additional data
|
||||||
const ad = uint8ArrayConcat([this.h, extraAd]);
|
const ad = uint8ArrayConcat([this.h, extraAd]);
|
||||||
@ -233,8 +310,12 @@ export class SymmetricState {
|
|||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Combines decryptWithAd and mixHash
|
* DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Combines decryptWithAd and mixHash
|
||||||
|
* @param ciphertext data to decrypt
|
||||||
|
* @param extraAd extra additional data
|
||||||
|
*/
|
||||||
decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
|
decryptAndHash(ciphertext: Uint8Array, extraAd: Uint8Array = new Uint8Array()): Uint8Array {
|
||||||
// The additional data
|
// The additional data
|
||||||
const ad = uint8ArrayConcat([this.h, extraAd]);
|
const ad = uint8ArrayConcat([this.h, extraAd]);
|
||||||
@ -246,11 +327,14 @@ export class SymmetricState {
|
|||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
/**
|
||||||
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
|
* Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||||
|
* Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
|
||||||
|
* @returns CipherState to encrypt and CipherState to decrypt
|
||||||
|
*/
|
||||||
split(): { cs1: CipherState; cs2: CipherState } {
|
split(): { cs1: CipherState; cs2: CipherState } {
|
||||||
// Derives 2 keys using HKDF and the chaining key
|
// Derives 2 keys using HKDF and the chaining key
|
||||||
const [tmpKey1, tmpKey2] = getHKDF(this.ck, new Uint8Array(0));
|
const [tmpKey1, tmpKey2] = HKDF(this.ck, new Uint8Array(0), 32, 2);
|
||||||
// Returns a tuple of two Cipher States initialized with the derived keys
|
// Returns a tuple of two Cipher States initialized with the derived keys
|
||||||
return {
|
return {
|
||||||
cs1: new CipherState(tmpKey1),
|
cs1: new CipherState(tmpKey1),
|
||||||
@ -258,17 +342,26 @@ export class SymmetricState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the chaining key field of a Symmetric State
|
/**
|
||||||
|
* Gets the chaining key field of a Symmetric State
|
||||||
|
* @returns Chaining key
|
||||||
|
*/
|
||||||
getChainingKey(): bytes32 {
|
getChainingKey(): bytes32 {
|
||||||
return this.ck;
|
return this.ck;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the handshake hash field of a Symmetric State
|
/**
|
||||||
|
* Gets the handshake hash field of a Symmetric State
|
||||||
|
* @returns Handshake hash
|
||||||
|
*/
|
||||||
getHandshakeHash(): bytes32 {
|
getHandshakeHash(): bytes32 {
|
||||||
return this.h;
|
return this.h;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the Cipher State field of a Symmetric State
|
/**
|
||||||
|
* Gets the Cipher State field of a Symmetric State
|
||||||
|
* @returns Cipher State
|
||||||
|
*/
|
||||||
getCipherState(): CipherState {
|
getCipherState(): CipherState {
|
||||||
return this.cs;
|
return this.cs;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { bytes, uint64 } from "./@types/basic.js";
|
// Adapted from https://github.com/ChainSafe/js-libp2p-noise/blob/master/src/nonce.ts
|
||||||
|
|
||||||
|
import type { uint64 } from "./@types/basic.js";
|
||||||
|
|
||||||
export const MIN_NONCE = 0;
|
export const MIN_NONCE = 0;
|
||||||
// For performance reasons, the nonce is represented as a JS `number`
|
// For performance reasons, the nonce is represented as a JS `number`
|
||||||
@ -17,7 +19,7 @@ const ERR_MAX_NONCE = "Cipherstate has reached maximum n, a new handshake must b
|
|||||||
*/
|
*/
|
||||||
export class Nonce {
|
export class Nonce {
|
||||||
private n: uint64;
|
private n: uint64;
|
||||||
private readonly bytes: bytes;
|
private readonly bytes: Uint8Array;
|
||||||
private readonly view: DataView;
|
private readonly view: DataView;
|
||||||
|
|
||||||
constructor(n = MIN_NONCE) {
|
constructor(n = MIN_NONCE) {
|
||||||
@ -33,7 +35,7 @@ export class Nonce {
|
|||||||
this.view.setUint32(4, this.n, true);
|
this.view.setUint32(4, this.n, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBytes(): bytes {
|
getBytes(): Uint8Array {
|
||||||
return this.bytes;
|
return this.bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@ import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
|||||||
|
|
||||||
import { NoiseHandshakeMessage } from "./codec";
|
import { NoiseHandshakeMessage } from "./codec";
|
||||||
import { generateX25519KeyPair } from "./crypto";
|
import { generateX25519KeyPair } from "./crypto";
|
||||||
|
import { MessageNametagBufferSize } from "./messagenametag";
|
||||||
import { ResponderParameters, WakuPairing } from "./pairing";
|
import { ResponderParameters, WakuPairing } from "./pairing";
|
||||||
import { MessageNametagBufferSize } from "./payload";
|
|
||||||
|
|
||||||
describe("js-noise: pairing object", () => {
|
describe("js-noise: pairing object", () => {
|
||||||
const rng = new HMACDRBG();
|
const rng = new HMACDRBG();
|
||||||
|
|||||||
@ -16,27 +16,48 @@ import {
|
|||||||
} from "./codec.js";
|
} from "./codec.js";
|
||||||
import { commitPublicKey, generateX25519KeyPair } from "./crypto.js";
|
import { commitPublicKey, generateX25519KeyPair } from "./crypto.js";
|
||||||
import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake.js";
|
import { Handshake, HandshakeResult, HandshakeStepResult, MessageNametagError } from "./handshake.js";
|
||||||
|
import { MessageNametagLength } from "./messagenametag.js";
|
||||||
import { NoiseHandshakePatterns } from "./patterns.js";
|
import { NoiseHandshakePatterns } from "./patterns.js";
|
||||||
import { MessageNametagLength } from "./payload.js";
|
|
||||||
import { NoisePublicKey } from "./publickey.js";
|
import { NoisePublicKey } from "./publickey.js";
|
||||||
import { QR } from "./qr.js";
|
import { QR } from "./qr.js";
|
||||||
|
|
||||||
const log = debug("waku:noise:pairing");
|
const log = debug("waku:noise:pairing");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sender interface that an object must implement so the pairing object can publish noise messages
|
||||||
|
*/
|
||||||
export interface Sender {
|
export interface Sender {
|
||||||
|
/**
|
||||||
|
* Publish a message
|
||||||
|
* @param encoder NoiseHandshakeEncoder encoder to use to encrypt the messages
|
||||||
|
* @param msg message to broadcast
|
||||||
|
*/
|
||||||
publish(encoder: Encoder, msg: Message): Promise<void>;
|
publish(encoder: Encoder, msg: Message): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responder interface than an object must implement so the pairing object can receive noise messages
|
||||||
|
*/
|
||||||
export interface Responder {
|
export interface Responder {
|
||||||
|
/**
|
||||||
|
* subscribe to receive the messages from a content topic
|
||||||
|
* @param decoder Decoder to use to decrypt the NoiseHandshakeMessages
|
||||||
|
*/
|
||||||
subscribe(decoder: Decoder<NoiseHandshakeMessage>): Promise<void>;
|
subscribe(decoder: Decoder<NoiseHandshakeMessage>): Promise<void>;
|
||||||
|
|
||||||
// next message should return messages received in a content topic
|
/**
|
||||||
// messages should be kept in a queue, meaning that nextMessage
|
* should return messages received in a content topic
|
||||||
// will call pop in the queue to remove the oldest message received
|
* messages should be kept in a queue, meaning that nextMessage
|
||||||
// (it's important to maintain order of received messages)
|
* will call pop in the queue to remove the oldest message received
|
||||||
|
* (it's important to maintain order of received messages)
|
||||||
|
* @param contentTopic content topic to get the next message from
|
||||||
|
*/
|
||||||
nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage>;
|
nextMessage(contentTopic: string): Promise<NoiseHandshakeMessage>;
|
||||||
|
|
||||||
// this should stop the subscription
|
/**
|
||||||
|
* Stop the subscription to the content topic
|
||||||
|
* @param contentTopic
|
||||||
|
*/
|
||||||
stop(contentTopic: string): Promise<void>;
|
stop(contentTopic: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,10 +67,16 @@ function delay(ms: number): Promise<void> {
|
|||||||
|
|
||||||
const rng = new HMACDRBG();
|
const rng = new HMACDRBG();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiator parameters used to setup the pairing object
|
||||||
|
*/
|
||||||
export class InitiatorParameters {
|
export class InitiatorParameters {
|
||||||
constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {}
|
constructor(public readonly qrCode: string, public readonly qrMessageNameTag: Uint8Array) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responder parameters used to setup the pairing object
|
||||||
|
*/
|
||||||
export class ResponderParameters {
|
export class ResponderParameters {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly applicationName: string = "waku-noise-sessions",
|
public readonly applicationName: string = "waku-noise-sessions",
|
||||||
@ -58,6 +85,9 @@ export class ResponderParameters {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairing object to setup a noise session
|
||||||
|
*/
|
||||||
export class WakuPairing {
|
export class WakuPairing {
|
||||||
public readonly contentTopic: string;
|
public readonly contentTopic: string;
|
||||||
|
|
||||||
@ -73,12 +103,24 @@ export class WakuPairing {
|
|||||||
|
|
||||||
private eventEmitter = new EventEmitter();
|
private eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a QR into a content topic
|
||||||
|
* @param qr
|
||||||
|
* @returns content topic string
|
||||||
|
*/
|
||||||
private static toContentTopic(qr: QR): string {
|
private static toContentTopic(qr: QR): string {
|
||||||
return (
|
return (
|
||||||
"/" + qr.applicationName + "/" + qr.applicationVersion + "/wakunoise/1/sessions_shard-" + qr.shardId + "/proto"
|
"/" + qr.applicationName + "/" + qr.applicationVersion + "/wakunoise/1/sessions_shard-" + qr.shardId + "/proto"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sender object that implements Sender interface to publish waku messages
|
||||||
|
* @param responder object that implements Responder interface to subscribe and receive waku messages
|
||||||
|
* @param myStaticKey x25519 keypair
|
||||||
|
* @param pairingParameters Pairing parameters (depending if this is the initiator or responder)
|
||||||
|
* @param myEphemeralKey optional ephemeral key
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private sender: Sender,
|
private sender: Sender,
|
||||||
private responder: Responder,
|
private responder: Responder,
|
||||||
@ -91,7 +133,7 @@ export class WakuPairing {
|
|||||||
|
|
||||||
if (pairingParameters instanceof InitiatorParameters) {
|
if (pairingParameters instanceof InitiatorParameters) {
|
||||||
this.initiator = true;
|
this.initiator = true;
|
||||||
this.qr = QR.fromString(pairingParameters.qrCode);
|
this.qr = QR.from(pairingParameters.qrCode);
|
||||||
this.qrMessageNameTag = pairingParameters.qrMessageNameTag;
|
this.qrMessageNameTag = pairingParameters.qrMessageNameTag;
|
||||||
} else {
|
} else {
|
||||||
this.initiator = false;
|
this.initiator = false;
|
||||||
@ -104,6 +146,7 @@ export class WakuPairing {
|
|||||||
this.myCommittedStaticKey
|
this.myCommittedStaticKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We set the contentTopic from the content topic parameters exchanged in the QR
|
// We set the contentTopic from the content topic parameters exchanged in the QR
|
||||||
this.contentTopic = WakuPairing.toContentTopic(this.qr);
|
this.contentTopic = WakuPairing.toContentTopic(this.qr);
|
||||||
|
|
||||||
@ -121,10 +164,19 @@ export class WakuPairing {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pairing information (as an InitiatorParameter object)
|
||||||
|
* @returns InitiatorParameters
|
||||||
|
*/
|
||||||
public getPairingInfo(): InitiatorParameters {
|
public getPairingInfo(): InitiatorParameters {
|
||||||
return new InitiatorParameters(this.qr.toString(), this.qrMessageNameTag);
|
return new InitiatorParameters(this.qr.toString(), this.qrMessageNameTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auth code (to validate that pairing). It must be displayed on both
|
||||||
|
* devices and the user(s) must confirm if the auth code match
|
||||||
|
* @returns Promise that resolves to an auth code
|
||||||
|
*/
|
||||||
public async getAuthCode(): Promise<string> {
|
public async getAuthCode(): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.authCode) {
|
if (this.authCode) {
|
||||||
@ -138,8 +190,14 @@ export class WakuPairing {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public validateAuthCode(confirmed: boolean): void {
|
/**
|
||||||
this.eventEmitter.emit("confirmAuthCode", confirmed);
|
* Indicate if auth code is valid. This is a function that must be
|
||||||
|
* manually called by the user(s) if the auth code in both devices being
|
||||||
|
* paired match. If false, pairing session is terminated
|
||||||
|
* @param isValid true if authcode is correct, false otherwise.
|
||||||
|
*/
|
||||||
|
public validateAuthCode(isValid: boolean): void {
|
||||||
|
this.eventEmitter.emit("confirmAuthCode", isValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isAuthCodeConfirmed(): Promise<boolean | undefined> {
|
private async isAuthCodeConfirmed(): Promise<boolean | undefined> {
|
||||||
@ -301,6 +359,13 @@ export class WakuPairing {
|
|||||||
return WakuPairing.getSecureCodec(this.contentTopic, this.handshakeResult);
|
return WakuPairing.getSecureCodec(this.contentTopic, this.handshakeResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get codecs for encoding/decoding messages in js-waku. This function can be used
|
||||||
|
* to continue a session using a stored hsResult
|
||||||
|
* @param contentTopic Content topic for the waku messages
|
||||||
|
* @param hsResult Noise Pairing result
|
||||||
|
* @returns an array with [NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]
|
||||||
|
*/
|
||||||
static getSecureCodec(
|
static getSecureCodec(
|
||||||
contentTopic: string,
|
contentTopic: string,
|
||||||
hsResult: HandshakeResult
|
hsResult: HandshakeResult
|
||||||
@ -311,6 +376,10 @@ export class WakuPairing {
|
|||||||
return [secureEncoder, secureDecoder];
|
return [secureEncoder, secureDecoder];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get handshake result
|
||||||
|
* @returns result of a successful pairing
|
||||||
|
*/
|
||||||
public getHandshakeResult(): HandshakeResult {
|
public getHandshakeResult(): HandshakeResult {
|
||||||
if (!this.handshakeResult) {
|
if (!this.handshakeResult) {
|
||||||
throw new Error("handshake is not complete");
|
throw new Error("handshake is not complete");
|
||||||
@ -318,14 +387,19 @@ export class WakuPairing {
|
|||||||
return this.handshakeResult;
|
return this.handshakeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(timeoutMs = 30000): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> {
|
/**
|
||||||
|
* Execute handshake
|
||||||
|
* @param timeoutMs Timeout in milliseconds after which the pairing session is invalid
|
||||||
|
* @returns promise that resolves to codecs for encoding/decoding messages in js-waku
|
||||||
|
*/
|
||||||
|
async execute(timeoutMs = 60000): Promise<[NoiseSecureTransferEncoder, NoiseSecureTransferDecoder]> {
|
||||||
if (this.started) {
|
if (this.started) {
|
||||||
throw new Error("pairing already executed. Create new pairing object");
|
throw new Error("pairing already executed. Create new pairing object");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.started = true;
|
this.started = true;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Limit QR exposure to 30s
|
// Limit QR exposure to some timeout
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
reject(new Error("pairing has timed out"));
|
reject(new Error("pairing has timed out"));
|
||||||
this.eventEmitter.emit("pairingTimeout");
|
this.eventEmitter.emit("pairingTimeout");
|
||||||
|
|||||||
114
src/patterns.ts
114
src/patterns.ts
@ -1,6 +1,7 @@
|
|||||||
// The Noise tokens appearing in Noise (pre)message patterns
|
/**
|
||||||
|
* The Noise tokens appearing in Noise (pre)message patterns
|
||||||
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
|
* as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
|
||||||
|
*/
|
||||||
export enum NoiseTokens {
|
export enum NoiseTokens {
|
||||||
e = "e",
|
e = "e",
|
||||||
s = "s",
|
s = "s",
|
||||||
@ -11,42 +12,61 @@ export enum NoiseTokens {
|
|||||||
psk = "psk",
|
psk = "psk",
|
||||||
}
|
}
|
||||||
|
|
||||||
// The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
|
/**
|
||||||
// as in http://www.noiseprotocol.org/noise.html#alice-and-bob
|
* The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
|
||||||
|
* as in http://www.noiseprotocol.org/noise.html#alice-and-bob
|
||||||
|
*/
|
||||||
export enum MessageDirection {
|
export enum MessageDirection {
|
||||||
r = "->",
|
r = "->",
|
||||||
l = "<-",
|
l = "<-",
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pre message pattern consisting of a message direction and some Noise tokens, if any.
|
/**
|
||||||
// (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
|
* The pre message pattern consisting of a message direction and some Noise tokens, if any.
|
||||||
|
* (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
|
||||||
|
*/
|
||||||
export class PreMessagePattern {
|
export class PreMessagePattern {
|
||||||
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
|
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
|
||||||
|
|
||||||
equals(b: PreMessagePattern): boolean {
|
/**
|
||||||
|
* Check PreMessagePattern equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: PreMessagePattern): boolean {
|
||||||
return (
|
return (
|
||||||
this.direction == b.direction &&
|
this.direction == other.direction &&
|
||||||
this.tokens.length === b.tokens.length &&
|
this.tokens.length === other.tokens.length &&
|
||||||
this.tokens.every((val, index) => val === b.tokens[index])
|
this.tokens.every((val, index) => val === other.tokens[index])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The message pattern consisting of a message direction and some Noise tokens
|
/**
|
||||||
// All Noise tokens are allowed
|
* The message pattern consisting of a message direction and some Noise tokens
|
||||||
|
* All Noise tokens are allowed
|
||||||
|
*/
|
||||||
export class MessagePattern {
|
export class MessagePattern {
|
||||||
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
|
constructor(public readonly direction: MessageDirection, public readonly tokens: Array<NoiseTokens>) {}
|
||||||
|
|
||||||
equals(b: MessagePattern): boolean {
|
/**
|
||||||
|
* Check MessagePattern equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: MessagePattern): boolean {
|
||||||
return (
|
return (
|
||||||
this.direction == b.direction &&
|
this.direction == other.direction &&
|
||||||
this.tokens.length === b.tokens.length &&
|
this.tokens.length === other.tokens.length &&
|
||||||
this.tokens.every((val, index) => val === b.tokens[index])
|
this.tokens.every((val, index) => val === other.tokens[index])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
|
/**
|
||||||
|
* The handshake pattern object. It stores the handshake protocol name, the
|
||||||
|
* handshake pre message patterns and the handshake message patterns
|
||||||
|
*/
|
||||||
export class HandshakePattern {
|
export class HandshakePattern {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
@ -54,25 +74,29 @@ export class HandshakePattern {
|
|||||||
public readonly messagePatterns: Array<MessagePattern>
|
public readonly messagePatterns: Array<MessagePattern>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
equals(b: HandshakePattern): boolean {
|
/**
|
||||||
if (this.preMessagePatterns.length != b.preMessagePatterns.length) return false;
|
* Check HandshakePattern equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: HandshakePattern): boolean {
|
||||||
|
if (this.preMessagePatterns.length != other.preMessagePatterns.length) return false;
|
||||||
for (let i = 0; i < this.preMessagePatterns.length; i++) {
|
for (let i = 0; i < this.preMessagePatterns.length; i++) {
|
||||||
if (!this.preMessagePatterns[i].equals(b.preMessagePatterns[i])) return false;
|
if (!this.preMessagePatterns[i].equals(other.preMessagePatterns[i])) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.messagePatterns.length != b.messagePatterns.length) return false;
|
if (this.messagePatterns.length != other.messagePatterns.length) return false;
|
||||||
for (let i = 0; i < this.messagePatterns.length; i++) {
|
for (let i = 0; i < this.messagePatterns.length; i++) {
|
||||||
if (!this.messagePatterns[i].equals(b.messagePatterns[i])) return false;
|
if (!this.messagePatterns[i].equals(other.messagePatterns[i])) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.name == b.name;
|
return this.name == other.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants (supported protocols)
|
/**
|
||||||
export const EmptyPreMessage = new Array<PreMessagePattern>();
|
* Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
|
||||||
|
*/
|
||||||
// Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
|
|
||||||
export const NoiseHandshakePatterns = {
|
export const NoiseHandshakePatterns = {
|
||||||
K1K1: new HandshakePattern(
|
K1K1: new HandshakePattern(
|
||||||
"Noise_K1K1_25519_ChaChaPoly_SHA256",
|
"Noise_K1K1_25519_ChaChaPoly_SHA256",
|
||||||
@ -95,16 +119,24 @@ export const NoiseHandshakePatterns = {
|
|||||||
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
XX: new HandshakePattern("Noise_XX_25519_ChaChaPoly_SHA256", EmptyPreMessage, [
|
XX: new HandshakePattern(
|
||||||
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
|
"Noise_XX_25519_ChaChaPoly_SHA256",
|
||||||
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
|
[],
|
||||||
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
[
|
||||||
]),
|
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
|
||||||
XXpsk0: new HandshakePattern("Noise_XXpsk0_25519_ChaChaPoly_SHA256", EmptyPreMessage, [
|
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
|
||||||
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]),
|
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
||||||
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
|
]
|
||||||
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
),
|
||||||
]),
|
XXpsk0: new HandshakePattern(
|
||||||
|
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]),
|
||||||
|
new MessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.ee, NoiseTokens.s, NoiseTokens.es]),
|
||||||
|
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
|
||||||
|
]
|
||||||
|
),
|
||||||
WakuPairing: new HandshakePattern(
|
WakuPairing: new HandshakePattern(
|
||||||
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
|
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
|
||||||
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
|
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
|
||||||
@ -116,8 +148,10 @@ export const NoiseHandshakePatterns = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supported Protocol ID for PayloadV2 objects
|
/**
|
||||||
// Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
|
* Supported Protocol ID for PayloadV2 objects
|
||||||
|
* Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
|
||||||
|
*/
|
||||||
export const PayloadV2ProtocolIDs: { [id: string]: number } = {
|
export const PayloadV2ProtocolIDs: { [id: string]: number } = {
|
||||||
"": 0,
|
"": 0,
|
||||||
Noise_K1K1_25519_ChaChaPoly_SHA256: 10,
|
Noise_K1K1_25519_ChaChaPoly_SHA256: 10,
|
||||||
|
|||||||
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 { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
||||||
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||||
|
|
||||||
import { MessageNametag } from "./@types/handshake.js";
|
import { MessageNametag } from "./@types/handshake.js";
|
||||||
import { ChachaPolyTagLen, Curve25519KeySize, hashSHA256 } from "./crypto.js";
|
import { ChachaPolyTagLen, Curve25519KeySize } from "./crypto.js";
|
||||||
|
import { MessageNametagLength } from "./messagenametag.js";
|
||||||
import { PayloadV2ProtocolIDs } from "./patterns.js";
|
import { PayloadV2ProtocolIDs } from "./patterns.js";
|
||||||
import { NoisePublicKey } from "./publickey.js";
|
import { NoisePublicKey } from "./publickey.js";
|
||||||
import { readUIntLE, writeUIntLE } from "./utils.js";
|
import { readUIntLE, writeUIntLE } from "./utils.js";
|
||||||
|
|
||||||
export const MessageNametagLength = 16;
|
/**
|
||||||
export const MessageNametagBufferSize = 50;
|
* PayloadV2 defines an object for Waku payloads with version 2 as in
|
||||||
|
* https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||||
// Converts a sequence or array (arbitrary size) to a MessageNametag
|
* It contains a message nametag, protocol ID field, the handshake message (for Noise handshakes)
|
||||||
export function toMessageNametag(input: Uint8Array): MessageNametag {
|
* and the transport message
|
||||||
return input.subarray(0, MessageNametagLength);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
export class MessageNametagBuffer {
|
|
||||||
private buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
|
|
||||||
private counter = 0;
|
|
||||||
secret?: Uint8Array;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
for (let i = 0; i < this.buffer.length; i++) {
|
|
||||||
this.buffer[i] = new Uint8Array(MessageNametagLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n )
|
|
||||||
initNametagsBuffer(): void {
|
|
||||||
// We default the counter and buffer fields
|
|
||||||
this.counter = 0;
|
|
||||||
this.buffer = new Array<MessageNametag>(MessageNametagBufferSize);
|
|
||||||
|
|
||||||
if (this.secret) {
|
|
||||||
for (let i = 0; i < this.buffer.length; i++) {
|
|
||||||
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
|
|
||||||
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
|
|
||||||
this.buffer[i] = toMessageNametag(d);
|
|
||||||
this.counter++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We warn users if no secret is set
|
|
||||||
console.debug("The message nametags buffer has not a secret set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pop(): MessageNametag {
|
|
||||||
// Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned
|
|
||||||
const messageNametag = new Uint8Array(this.buffer[0]);
|
|
||||||
this.delete(1);
|
|
||||||
return messageNametag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the input messageNametag is contained in the input MessageNametagBuffer
|
|
||||||
checkNametag(messageNametag: MessageNametag): boolean {
|
|
||||||
const index = this.buffer.findIndex((x) => uint8ArrayEquals(x, messageNametag));
|
|
||||||
|
|
||||||
if (index == -1) {
|
|
||||||
console.debug("Message nametag not found in buffer");
|
|
||||||
return false;
|
|
||||||
} else if (index > 0) {
|
|
||||||
console.debug(
|
|
||||||
"Message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost"
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// index is 0, hence the read message tag is the next expected one
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateLeft(k: number): void {
|
|
||||||
if (k < 0 || this.buffer.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const idx = this.buffer.length - (k % this.buffer.length);
|
|
||||||
const a1 = this.buffer.slice(idx);
|
|
||||||
const a2 = this.buffer.slice(0, idx);
|
|
||||||
this.buffer = a1.concat(a2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes the first n elements in buffer and appends n new ones
|
|
||||||
delete(n: number): void {
|
|
||||||
if (n <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced)
|
|
||||||
n = Math.min(n, MessageNametagBufferSize);
|
|
||||||
|
|
||||||
// We update the last n values in the array if a secret is set
|
|
||||||
// Note that if the input MessageNametagBuffer is set to default, nothing is done here
|
|
||||||
if (this.secret) {
|
|
||||||
// We rotate left the array by n
|
|
||||||
this.rotateLeft(n);
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
const counterBytesLE = writeUIntLE(new Uint8Array(8), this.counter, 0, 8);
|
|
||||||
const d = hashSHA256(uint8ArrayConcat([this.secret, counterBytesLE]));
|
|
||||||
|
|
||||||
this.buffer[this.buffer.length - n + i] = toMessageNametag(d);
|
|
||||||
this.counter++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We warn users that no secret is set
|
|
||||||
console.debug("The message nametags buffer has no secret set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PayloadV2 {
|
export class PayloadV2 {
|
||||||
messageNametag: MessageNametag;
|
messageNametag: MessageNametag;
|
||||||
protocolId: number;
|
protocolId: number;
|
||||||
@ -131,6 +32,10 @@ export class PayloadV2 {
|
|||||||
this.transportMessage = transportMessage;
|
this.transportMessage = transportMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the PayloadV2
|
||||||
|
* @returns a copy of the PayloadV2
|
||||||
|
*/
|
||||||
clone(): PayloadV2 {
|
clone(): PayloadV2 {
|
||||||
const r = new PayloadV2();
|
const r = new PayloadV2();
|
||||||
r.protocolId = this.protocolId;
|
r.protocolId = this.protocolId;
|
||||||
@ -142,31 +47,39 @@ export class PayloadV2 {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(b: PayloadV2): boolean {
|
/**
|
||||||
|
* Check PayloadV2 equality
|
||||||
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: PayloadV2): boolean {
|
||||||
let pkEquals = true;
|
let pkEquals = true;
|
||||||
if (this.handshakeMessage.length != b.handshakeMessage.length) {
|
if (this.handshakeMessage.length != other.handshakeMessage.length) {
|
||||||
pkEquals = false;
|
pkEquals = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.handshakeMessage.length; i++) {
|
for (let i = 0; i < this.handshakeMessage.length; i++) {
|
||||||
if (!this.handshakeMessage[i].equals(b.handshakeMessage[i])) {
|
if (!this.handshakeMessage[i].equals(other.handshakeMessage[i])) {
|
||||||
pkEquals = false;
|
pkEquals = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
uint8ArrayEquals(this.messageNametag, b.messageNametag) &&
|
uint8ArrayEquals(this.messageNametag, other.messageNametag) &&
|
||||||
this.protocolId == b.protocolId &&
|
this.protocolId == other.protocolId &&
|
||||||
uint8ArrayEquals(this.transportMessage, b.transportMessage) &&
|
uint8ArrayEquals(this.transportMessage, other.transportMessage) &&
|
||||||
pkEquals
|
pkEquals
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/.
|
/**
|
||||||
// The output serialized payload concatenates the input PayloadV2 object fields as
|
* Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/.
|
||||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
* The output serialized payload concatenates the input PayloadV2 object fields as
|
||||||
// The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
|
* payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||||
|
* The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
|
||||||
|
* @returns serialized payload
|
||||||
|
*/
|
||||||
serialize(): Uint8Array {
|
serialize(): Uint8Array {
|
||||||
// We collect public keys contained in the handshake message
|
// We collect public keys contained in the handshake message
|
||||||
|
|
||||||
@ -210,9 +123,11 @@ export class PayloadV2 {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
|
/**
|
||||||
// The input serialized payload concatenates the output PayloadV2 object fields as
|
* Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
|
||||||
// payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
* @param payload input serialized payload
|
||||||
|
* @returns PayloadV2
|
||||||
|
*/
|
||||||
static deserialize(payload: Uint8Array): PayloadV2 {
|
static deserialize(payload: Uint8Array): PayloadV2 {
|
||||||
// i is the read input buffer position index
|
// i is the read input buffer position index
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|||||||
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 { chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt } from "./crypto.js";
|
||||||
import { isEmptyKey } from "./noise.js";
|
import { isEmptyKey } from "./noise.js";
|
||||||
|
|
||||||
// A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
|
/**
|
||||||
|
* A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
|
||||||
|
*/
|
||||||
export class ChaChaPolyCipherState {
|
export class ChaChaPolyCipherState {
|
||||||
k: bytes32;
|
k: bytes32;
|
||||||
nonce: bytes32;
|
nonce: bytes32;
|
||||||
ad: Uint8Array;
|
ad: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param k 32-byte key
|
||||||
|
* @param nonce 12 byte little-endian nonce
|
||||||
|
* @param ad associated data
|
||||||
|
*/
|
||||||
constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) {
|
constructor(k: bytes32 = new Uint8Array(), nonce: bytes32 = new Uint8Array(), ad: Uint8Array = new Uint8Array()) {
|
||||||
this.k = k;
|
this.k = k;
|
||||||
this.nonce = nonce;
|
this.nonce = nonce;
|
||||||
this.ad = ad;
|
this.ad = ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
// It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext
|
/**
|
||||||
// The cipher state in not changed
|
* Takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext.
|
||||||
|
* The cipher state in not changed
|
||||||
|
* @param plaintext data to encrypt
|
||||||
|
* @returns sealed ciphertext including authentication tag
|
||||||
|
*/
|
||||||
encrypt(plaintext: Uint8Array): Uint8Array {
|
encrypt(plaintext: Uint8Array): Uint8Array {
|
||||||
// If plaintext is empty, we raise an error
|
// If plaintext is empty, we raise an error
|
||||||
if (plaintext.length == 0) {
|
if (plaintext.length == 0) {
|
||||||
@ -27,9 +39,12 @@ export class ChaChaPolyCipherState {
|
|||||||
return chaCha20Poly1305Encrypt(plaintext, this.nonce, this.ad, this.k);
|
return chaCha20Poly1305Encrypt(plaintext, this.nonce, this.ad, this.k);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChaChaPoly decryption
|
/**
|
||||||
// It takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
|
* Takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
|
||||||
// The cipher state is not changed
|
* The cipher state is not changed
|
||||||
|
* @param ciphertext data to decrypt
|
||||||
|
* @returns plaintext
|
||||||
|
*/
|
||||||
decrypt(ciphertext: Uint8Array): Uint8Array {
|
decrypt(ciphertext: Uint8Array): Uint8Array {
|
||||||
// If ciphertext is empty, we raise an error
|
// If ciphertext is empty, we raise an error
|
||||||
if (ciphertext.length == 0) {
|
if (ciphertext.length == 0) {
|
||||||
@ -44,30 +59,49 @@ export class ChaChaPolyCipherState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Noise public key is a public key exchanged during Noise handshakes (no private part)
|
/**
|
||||||
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
|
* A Noise public key is a public key exchanged during Noise handshakes (no private part)
|
||||||
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
|
* This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||||
// or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1)
|
*/
|
||||||
// Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
|
|
||||||
export class NoisePublicKey {
|
export class NoisePublicKey {
|
||||||
|
/**
|
||||||
|
* @param flag 1 to indicate that the public key is encrypted, 0 for unencrypted.
|
||||||
|
* Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
|
||||||
|
* @param pk contains the X coordinate of the public key, if unencrypted
|
||||||
|
* or the encryption of the X coordinate concatenated with the authorization tag, if encrypted
|
||||||
|
*/
|
||||||
constructor(public readonly flag: number, public readonly pk: Uint8Array) {}
|
constructor(public readonly flag: number, public readonly pk: Uint8Array) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the NoisePublicKey
|
||||||
|
* @returns a copy of the NoisePublicKey
|
||||||
|
*/
|
||||||
clone(): NoisePublicKey {
|
clone(): NoisePublicKey {
|
||||||
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
|
return new NoisePublicKey(this.flag, new Uint8Array(this.pk));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks equality between two Noise public keys
|
/**
|
||||||
equals(k2: NoisePublicKey): boolean {
|
* Check NoisePublicKey equality
|
||||||
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk);
|
* @param other object to compare against
|
||||||
|
* @returns true if equal, false otherwise
|
||||||
|
*/
|
||||||
|
equals(other: NoisePublicKey): boolean {
|
||||||
|
return this.flag == other.flag && uint8ArrayEquals(this.pk, other.pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a public Elliptic Curve key to an unencrypted Noise public key
|
/**
|
||||||
|
* Converts a public Elliptic Curve key to an unencrypted Noise public key
|
||||||
|
* @param publicKey 32-byte public key
|
||||||
|
* @returns NoisePublicKey
|
||||||
|
*/
|
||||||
static fromPublicKey(publicKey: bytes32): NoisePublicKey {
|
static fromPublicKey(publicKey: bytes32): NoisePublicKey {
|
||||||
return new NoisePublicKey(0, publicKey);
|
return new NoisePublicKey(0, publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a Noise public key to a stream of bytes as in
|
/**
|
||||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
* Converts a Noise public key to a stream of bytes as in https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||||
|
* @returns Serialized NoisePublicKey
|
||||||
|
*/
|
||||||
serialize(): Uint8Array {
|
serialize(): Uint8Array {
|
||||||
// Public key is serialized as (flag || pk)
|
// Public key is serialized as (flag || pk)
|
||||||
// Note that pk contains the X coordinate of the public key if unencrypted
|
// Note that pk contains the X coordinate of the public key if unencrypted
|
||||||
@ -76,8 +110,11 @@ export class NoisePublicKey {
|
|||||||
return serializedNoisePublicKey;
|
return serializedNoisePublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a serialized Noise public key to a NoisePublicKey object as in
|
/**
|
||||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
* Converts a serialized Noise public key to a NoisePublicKey object as in https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||||
|
* @param serializedPK Serialized NoisePublicKey
|
||||||
|
* @returns NoisePublicKey
|
||||||
|
*/
|
||||||
static deserialize(serializedPK: Uint8Array): NoisePublicKey {
|
static deserialize(serializedPK: Uint8Array): NoisePublicKey {
|
||||||
if (serializedPK.length == 0) throw new Error("invalid serialized key");
|
if (serializedPK.length == 0) throw new Error("invalid serialized key");
|
||||||
|
|
||||||
@ -90,30 +127,41 @@ export class NoisePublicKey {
|
|||||||
return new NoisePublicKey(flag, pk);
|
return new NoisePublicKey(flag, pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
static encrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
/**
|
||||||
|
* Encrypt a NoisePublicKey using a ChaChaPolyCipherState
|
||||||
|
* @param pk NoisePublicKey to encrypt
|
||||||
|
* @param cs ChaChaPolyCipherState used to encrypt
|
||||||
|
* @returns encrypted NoisePublicKey
|
||||||
|
*/
|
||||||
|
static encrypt(pk: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
||||||
// We proceed with encryption only if
|
// We proceed with encryption only if
|
||||||
// - a key is set in the cipher state
|
// - a key is set in the cipher state
|
||||||
// - the public key is unencrypted
|
// - the public key is unencrypted
|
||||||
if (!isEmptyKey(cs.k) && ns.flag == 0) {
|
if (!isEmptyKey(cs.k) && pk.flag == 0) {
|
||||||
const encPk = cs.encrypt(ns.pk);
|
const encPk = cs.encrypt(pk.pk);
|
||||||
return new NoisePublicKey(1, encPk);
|
return new NoisePublicKey(1, encPk);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we return the public key as it is
|
// Otherwise we return the public key as it is
|
||||||
return ns.clone();
|
return pk.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypts a Noise public key using a ChaChaPoly Cipher State
|
/**
|
||||||
static decrypt(ns: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
* Decrypts a Noise public key using a ChaChaPoly Cipher State
|
||||||
|
* @param pk NoisePublicKey to decrypt
|
||||||
|
* @param cs ChaChaPolyCipherState used to decrypt
|
||||||
|
* @returns decrypted NoisePublicKey
|
||||||
|
*/
|
||||||
|
static decrypt(pk: NoisePublicKey, cs: ChaChaPolyCipherState): NoisePublicKey {
|
||||||
// We proceed with decryption only if
|
// We proceed with decryption only if
|
||||||
// - a key is set in the cipher state
|
// - a key is set in the cipher state
|
||||||
// - the public key is encrypted
|
// - the public key is encrypted
|
||||||
if (!isEmptyKey(cs.k) && ns.flag == 1) {
|
if (!isEmptyKey(cs.k) && pk.flag == 1) {
|
||||||
const decrypted = cs.decrypt(ns.pk);
|
const decrypted = cs.decrypt(pk.pk);
|
||||||
return new NoisePublicKey(0, decrypted);
|
return new NoisePublicKey(0, decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we return the public key as it is
|
// Otherwise we return the public key as it is
|
||||||
return ns.clone();
|
return pk.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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";
|
import { bytes32 } from "./@types/basic.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QR code generation
|
||||||
|
*/
|
||||||
export class QR {
|
export class QR {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly applicationName: string,
|
public readonly applicationName: string,
|
||||||
@ -22,14 +25,30 @@ export class QR {
|
|||||||
return qr;
|
return qr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert QR code into byte array
|
||||||
|
* @returns byte array serialization of a base64 encoded QR code
|
||||||
|
*/
|
||||||
toByteArray(): Uint8Array {
|
toByteArray(): Uint8Array {
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
return enc.encode(this.toString());
|
return enc.encode(this.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
|
/**
|
||||||
static fromString(qrString: string): QR {
|
* Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
|
||||||
const values = qrString.split(":");
|
* @param input input base64 encoded string
|
||||||
|
* @returns QR
|
||||||
|
*/
|
||||||
|
static from(input: string | Uint8Array): QR {
|
||||||
|
let qrStr: string;
|
||||||
|
if (input instanceof Uint8Array) {
|
||||||
|
const dec = new TextDecoder();
|
||||||
|
qrStr = dec.decode(input);
|
||||||
|
} else {
|
||||||
|
qrStr = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = qrStr.split(":");
|
||||||
|
|
||||||
if (values.length != 5) throw new Error("invalid qr string");
|
if (values.length != 5) throw new Error("invalid qr string");
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import {
|
|||||||
} from "./codec";
|
} from "./codec";
|
||||||
import { commitPublicKey, generateX25519KeyPair } from "./crypto";
|
import { commitPublicKey, generateX25519KeyPair } from "./crypto";
|
||||||
import { Handshake } from "./handshake";
|
import { Handshake } from "./handshake";
|
||||||
|
import { MessageNametagBufferSize, MessageNametagLength } from "./messagenametag";
|
||||||
import { NoiseHandshakePatterns } from "./patterns";
|
import { NoiseHandshakePatterns } from "./patterns";
|
||||||
import { MessageNametagBufferSize, MessageNametagLength } from "./payload";
|
|
||||||
import { NoisePublicKey } from "./publickey";
|
import { NoisePublicKey } from "./publickey";
|
||||||
import { QR } from "./qr";
|
import { QR } from "./qr";
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ describe("Waku Noise Sessions", () => {
|
|||||||
const qr = new QR(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey);
|
const qr = new QR(applicationName, applicationVersion, shardId, bobEphemeralKey.publicKey, bobCommittedStaticKey);
|
||||||
|
|
||||||
// Alice deserializes the QR code
|
// Alice deserializes the QR code
|
||||||
const readQR = QR.fromString(qr.toString());
|
const readQR = QR.from(qr.toString());
|
||||||
|
|
||||||
// We check if QR serialization/deserialization works
|
// We check if QR serialization/deserialization works
|
||||||
expect(readQR.applicationName).to.be.equals(applicationName);
|
expect(readQR.applicationName).to.be.equals(applicationName);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user