feat: payloadv2 serialization and QR utils

This commit is contained in:
Richard Ramos 2022-11-16 15:59:23 -04:00
parent af283927a4
commit aa2490825b
No known key found for this signature in database
GPG Key ID: BD36D48BC9FFC88C
5 changed files with 240 additions and 37 deletions

View File

@ -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]));
}

View File

@ -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

View File

@ -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();

View File

@ -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
View 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 };
}