mirror of
https://github.com/waku-org/js-noise.git
synced 2025-02-24 08:58:15 +00:00
feat: payloadv2 serialization and QR utils
This commit is contained in:
parent
af283927a4
commit
aa2490825b
@ -1,13 +1,15 @@
|
||||
import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305";
|
||||
import { ChaCha20Poly1305, TAG_LENGTH } from "@stablelib/chacha20poly1305";
|
||||
import { HKDF } from "@stablelib/hkdf";
|
||||
import { hash, SHA256 } from "@stablelib/sha256";
|
||||
import * as x25519 from "@stablelib/x25519";
|
||||
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
||||
|
||||
import type { bytes, bytes32 } from "./@types/basic.js";
|
||||
import type { Hkdf } from "./@types/handshake.js";
|
||||
import type { KeyPair } from "./@types/keypair.js";
|
||||
|
||||
export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH;
|
||||
export const ChachaPolyTagLen = TAG_LENGTH;
|
||||
|
||||
export function hashSHA256(data: Uint8Array): Uint8Array {
|
||||
return hash(data);
|
||||
@ -86,3 +88,8 @@ export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
|
||||
return new Uint8Array(32);
|
||||
}
|
||||
}
|
||||
|
||||
// Commits a public key pk for randomness r as H(pk || s)
|
||||
export function commitPublicKey(publicKey: bytes32, r: Uint8Array): bytes32 {
|
||||
return hashSHA256(uint8ArrayConcat([publicKey, r]));
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import * as pkcs7 from "pkcs7-padding";
|
||||
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||
|
||||
import { bytes32 } from "./@types/basic.js";
|
||||
import { MessageNametag } from "./@types/handshake.js";
|
||||
import type { KeyPair } from "./@types/keypair.js";
|
||||
import { Curve25519KeySize, dh, generateX25519KeyPair, getHKDF, intoCurve25519Key } from "./crypto.js";
|
||||
import { SymmetricState } from "./noise.js";
|
||||
@ -100,6 +101,13 @@ export class HandshakeState {
|
||||
return { nms1, nms2 };
|
||||
}
|
||||
|
||||
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag
|
||||
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
|
||||
toMessageNametag(): MessageNametag {
|
||||
const [output] = getHKDF(this.ss.h, new Uint8Array());
|
||||
return output;
|
||||
}
|
||||
|
||||
// Handshake Processing
|
||||
|
||||
// Based on the message handshake direction and if the user is or not the initiator, returns a boolean tuple telling if the user
|
||||
|
@ -8,7 +8,7 @@ import { Handshake, HandshakeStepResult } from "./handshake";
|
||||
import { CipherState, createEmptyKey, SymmetricState } from "./noise";
|
||||
import { MAX_NONCE, Nonce } from "./nonce";
|
||||
import { NoiseHandshakePatterns } from "./patterns";
|
||||
import { MessageNametagBuffer } from "./payload";
|
||||
import { MessageNametagBuffer, MessageNametagLength, PayloadV2 } from "./payload";
|
||||
import { ChaChaPolyCipherState, NoisePublicKey } from "./publickey";
|
||||
|
||||
function randomCipherState(rng: HMACDRBG, nonce: number = 0): CipherState {
|
||||
@ -34,6 +34,14 @@ function randomNoisePublicKey(): NoisePublicKey {
|
||||
return new NoisePublicKey(0, keypair.publicKey);
|
||||
}
|
||||
|
||||
function randomPayloadV2(rng: HMACDRBG): PayloadV2 {
|
||||
const messageNametag = randomBytes(MessageNametagLength, rng);
|
||||
const protocolId = Math.floor(Math.random() * 255);
|
||||
const handshakeMessage = [randomNoisePublicKey(), randomNoisePublicKey(), randomNoisePublicKey()];
|
||||
const transportMessage = randomBytes(128);
|
||||
return new PayloadV2(messageNametag, protocolId, handshakeMessage, transportMessage);
|
||||
}
|
||||
|
||||
describe("js-noise", () => {
|
||||
const rng = new HMACDRBG();
|
||||
|
||||
@ -119,6 +127,13 @@ describe("js-noise", () => {
|
||||
expect(noisePublicKey.equals(decryptedPK)).to.be.true;
|
||||
});
|
||||
|
||||
it("PayloadV2: serialize/deserialize PayloadV2 to byte sequence", function () {
|
||||
const payload2 = randomPayloadV2(rng);
|
||||
const serializedPayload = payload2.serialize();
|
||||
const deserializedPayload = PayloadV2.deserialize(serializedPayload);
|
||||
expect(deserializedPayload.equals(payload2)).to.be.true;
|
||||
});
|
||||
|
||||
it("Noise State Machine: Diffie-Hellman operation", function () {
|
||||
const aliceKey = generateX25519KeyPair();
|
||||
const bobKey = generateX25519KeyPair();
|
||||
|
151
src/payload.ts
151
src/payload.ts
@ -6,49 +6,18 @@ import { concat as uint8ArrayConcat } from "uint8arrays/concat";
|
||||
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
|
||||
|
||||
import { MessageNametag } from "./@types/handshake";
|
||||
import { hashSHA256 } from "./crypto";
|
||||
import { ChachaPolyTagLen, Curve25519KeySize, hashSHA256 } from "./crypto";
|
||||
import { NoisePublicKey } from "./publickey";
|
||||
import { readUIntLE, writeUIntLE } from "./utils";
|
||||
|
||||
const MessageNametagLength = 16;
|
||||
const MessageNametagBufferSize = 50;
|
||||
export const MessageNametagLength = 16;
|
||||
export const MessageNametagBufferSize = 50;
|
||||
|
||||
// Converts a sequence or array (arbitrary size) to a MessageNametag
|
||||
export function toMessageNametag(input: Uint8Array): MessageNametag {
|
||||
return input.subarray(0, MessageNametagLength);
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/feross/buffer
|
||||
|
||||
function checkInt(buf: Uint8Array, value: number, offset: number, ext: number, max: number, min: number): void {
|
||||
if (value > max || value < min) throw new RangeError('"value" argument is out of bounds');
|
||||
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
||||
}
|
||||
|
||||
const writeUIntLE = function writeUIntLE(
|
||||
buf: Uint8Array,
|
||||
value: number,
|
||||
offset: number,
|
||||
byteLength: number,
|
||||
noAssert?: boolean
|
||||
): Uint8Array {
|
||||
value = +value;
|
||||
offset = offset >>> 0;
|
||||
byteLength = byteLength >>> 0;
|
||||
if (!noAssert) {
|
||||
const maxBytes = Math.pow(2, 8 * byteLength) - 1;
|
||||
checkInt(buf, value, offset, byteLength, maxBytes, 0);
|
||||
}
|
||||
|
||||
let mul = 1;
|
||||
let i = 0;
|
||||
buf[offset] = value & 0xff;
|
||||
while (++i < byteLength && (mul *= 0x100)) {
|
||||
buf[offset + i] = (value / mul) & 0xff;
|
||||
}
|
||||
|
||||
return buf;
|
||||
};
|
||||
|
||||
export class MessageNametagBuffer {
|
||||
buffer: Array<MessageNametag> = new Array<MessageNametag>(MessageNametagBufferSize);
|
||||
counter = 0;
|
||||
@ -179,4 +148,116 @@ export class PayloadV2 {
|
||||
pkEquals
|
||||
);
|
||||
}
|
||||
|
||||
// Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/.
|
||||
// The output serialized payload concatenates the input PayloadV2 object fields as
|
||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
// The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
|
||||
serialize(): Uint8Array {
|
||||
// We collect public keys contained in the handshake message
|
||||
|
||||
// According to https://rfc.vac.dev/spec/35/, the maximum size for the handshake message is 256 bytes, that is
|
||||
// the handshake message length can be represented with 1 byte only. (its length can be stored in 1 byte)
|
||||
// However, to ease public keys length addition operation, we declare it as int and later cast to uit8
|
||||
let serializedHandshakeMessageLen = 0;
|
||||
// This variables will store the concatenation of the serializations of all public keys in the handshake message
|
||||
let serializedHandshakeMessage = new Uint8Array();
|
||||
|
||||
// For each public key in the handshake message
|
||||
for (const pk of this.handshakeMessage) {
|
||||
// We serialize the public key
|
||||
const serializedPk = pk.serialize();
|
||||
// We sum its serialized length to the total
|
||||
serializedHandshakeMessageLen += serializedPk.length;
|
||||
// We add its serialization to the concatenation of all serialized public keys in the handshake message
|
||||
serializedHandshakeMessage = uint8ArrayConcat([serializedHandshakeMessage, serializedPk]);
|
||||
// If we are processing more than 256 byte, we return an error
|
||||
if (serializedHandshakeMessageLen > 255) {
|
||||
console.debug("PayloadV2 malformed: too many public keys contained in the handshake message");
|
||||
throw new Error("too many public keys in handshake message");
|
||||
}
|
||||
}
|
||||
|
||||
// The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as
|
||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
|
||||
// We concatenate all the data
|
||||
// The protocol ID (1 byte) and handshake message length (1 byte) can be directly casted to byte to allow direct copy to the payload byte sequence
|
||||
const payload = uint8ArrayConcat([
|
||||
this.messageNametag,
|
||||
new Uint8Array([this.protocolId]),
|
||||
new Uint8Array([serializedHandshakeMessageLen]),
|
||||
serializedHandshakeMessage,
|
||||
// The transport message length is converted from uint64 to bytes in Little-Endian
|
||||
writeUIntLE(new Uint8Array(8), this.transportMessage.length, 0, 8),
|
||||
this.transportMessage,
|
||||
]);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
|
||||
// The input serialized payload concatenates the output PayloadV2 object fields as
|
||||
// payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
static deserialize(payload: Uint8Array): PayloadV2 {
|
||||
// i is the read input buffer position index
|
||||
let i = 0;
|
||||
|
||||
// We start by reading the messageNametag
|
||||
const messageNametag = new Uint8Array(MessageNametagLength);
|
||||
for (let j = 0; j < MessageNametagLength; j++) {
|
||||
messageNametag[j] = payload[i + j];
|
||||
}
|
||||
i += MessageNametagLength;
|
||||
|
||||
// We read the Protocol ID
|
||||
// TODO: when the list of supported protocol ID is defined, check if read protocol ID is supported
|
||||
const protocolId = payload[i];
|
||||
i++;
|
||||
|
||||
// We read the Handshake Message length (1 byte)
|
||||
const handshakeMessageLen = payload[i];
|
||||
if (handshakeMessageLen > 255) {
|
||||
console.debug("payload malformed: too many public keys contained in the handshake message");
|
||||
throw new Error("too many public keys in handshake message");
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
// We now read for handshakeMessageLen bytes the buffer and we deserialize each (encrypted/unencrypted) public key read
|
||||
// In handshakeMessage we accumulate the read deserialized Noise Public keys
|
||||
const handshakeMessage = new Array<NoisePublicKey>();
|
||||
let written = 0;
|
||||
|
||||
// We read the buffer until handshakeMessageLen are read
|
||||
while (written != handshakeMessageLen) {
|
||||
// We obtain the current Noise Public key encryption flag
|
||||
const flag = payload[i];
|
||||
// If the key is unencrypted, we only read the X coordinate of the EC public key and we deserialize into a Noise Public Key
|
||||
if (flag === 0) {
|
||||
const pkLen = 1 + Curve25519KeySize;
|
||||
handshakeMessage.push(NoisePublicKey.deserialize(payload.subarray(i, i + pkLen)));
|
||||
i += pkLen;
|
||||
written += pkLen;
|
||||
// If the key is encrypted, we only read the encrypted X coordinate and the authorization tag, and we deserialize into a Noise Public Key
|
||||
} else if (flag === 1) {
|
||||
const pkLen = 1 + Curve25519KeySize + ChachaPolyTagLen;
|
||||
handshakeMessage.push(NoisePublicKey.deserialize(payload.subarray(i, i + pkLen)));
|
||||
i += pkLen;
|
||||
written += pkLen;
|
||||
} else {
|
||||
throw new Error("invalid flag for Noise public key");
|
||||
}
|
||||
}
|
||||
|
||||
// We read the transport message length (8 bytes) and we convert to uint64 in Little Endian
|
||||
const transportMessageLen = readUIntLE(payload, i, i + 8 - 1);
|
||||
i += 8;
|
||||
|
||||
// We read the transport message (handshakeMessage bytes)
|
||||
const transportMessage = payload.subarray(i, i + transportMessageLen);
|
||||
i += transportMessageLen;
|
||||
|
||||
return new PayloadV2(messageNametag, protocolId, handshakeMessage, transportMessage);
|
||||
}
|
||||
}
|
||||
|
92
src/utils.ts
Normal file
92
src/utils.ts
Normal file
@ -0,0 +1,92 @@
|
||||
// Adapted from https://github.com/feross/buffer
|
||||
|
||||
import { bytes32 } from "./@types/basic";
|
||||
|
||||
function checkInt(buf: Uint8Array, value: number, offset: number, ext: number, max: number, min: number): void {
|
||||
if (value > max || value < min) throw new RangeError('"value" argument is out of bounds');
|
||||
if (offset + ext > buf.length) throw new RangeError("Index out of range");
|
||||
}
|
||||
|
||||
export function writeUIntLE(
|
||||
buf: Uint8Array,
|
||||
value: number,
|
||||
offset: number,
|
||||
byteLength: number,
|
||||
noAssert?: boolean
|
||||
): Uint8Array {
|
||||
value = +value;
|
||||
offset = offset >>> 0;
|
||||
byteLength = byteLength >>> 0;
|
||||
if (!noAssert) {
|
||||
const maxBytes = Math.pow(2, 8 * byteLength) - 1;
|
||||
checkInt(buf, value, offset, byteLength, maxBytes, 0);
|
||||
}
|
||||
|
||||
let mul = 1;
|
||||
let i = 0;
|
||||
buf[offset] = value & 0xff;
|
||||
while (++i < byteLength && (mul *= 0x100)) {
|
||||
buf[offset + i] = (value / mul) & 0xff;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
function checkOffset(offset: number, ext: number, length: number): void {
|
||||
if (offset % 1 !== 0 || offset < 0) throw new RangeError("offset is not uint");
|
||||
if (offset + ext > length) throw new RangeError("Trying to access beyond buffer length");
|
||||
}
|
||||
|
||||
export function readUIntLE(buf: Uint8Array, offset: number, byteLength: number, noAssert?: boolean): number {
|
||||
offset = offset >>> 0;
|
||||
byteLength = byteLength >>> 0;
|
||||
if (!noAssert) checkOffset(offset, byteLength, buf.length);
|
||||
|
||||
let val = buf[offset];
|
||||
let mul = 1;
|
||||
let i = 0;
|
||||
while (++i < byteLength && (mul *= 0x100)) {
|
||||
val += buf[offset + i] * mul;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
// Serializes input parameters to a base64 string for exposure through QR code (used by WakuPairing)
|
||||
export function toQr(
|
||||
applicationName: string,
|
||||
applicationVersion: string,
|
||||
shardId: string,
|
||||
ephemeralKey: bytes32,
|
||||
committedStaticKey: bytes32
|
||||
): string {
|
||||
const decoder = new TextDecoder("utf8");
|
||||
let qr = window.btoa(applicationName) + ":";
|
||||
qr += window.btoa(applicationVersion) + ":";
|
||||
qr += window.btoa(shardId) + ":";
|
||||
qr += window.btoa(decoder.decode(ephemeralKey)) + ":";
|
||||
qr += window.btoa(decoder.decode(committedStaticKey));
|
||||
return qr;
|
||||
}
|
||||
|
||||
// Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
|
||||
export function fromQr(qr: string): {
|
||||
applicationName: string;
|
||||
applicationVersion: string;
|
||||
shardId: string;
|
||||
ephemeralKey: bytes32;
|
||||
committedStaticKey: bytes32;
|
||||
} {
|
||||
const values = qr.split(":");
|
||||
|
||||
if (values.length != 5) throw new Error("invalid qr string");
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const applicationName = window.atob(values[0]);
|
||||
const applicationVersion = window.atob(values[1]);
|
||||
const shardId = window.atob(values[2]);
|
||||
const ephemeralKey = encoder.encode(window.atob(values[3]));
|
||||
const committedStaticKey = encoder.encode(window.atob(values[4]));
|
||||
|
||||
return { applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey };
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user