chore: make message-encryption compile

This commit is contained in:
fryorcraken.eth 2022-11-04 11:38:32 +11:00
parent 256b7223f3
commit e6efd0438c
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
9 changed files with 1063 additions and 13 deletions

83
package-lock.json generated
View File

@ -5471,6 +5471,10 @@
"resolved": "packages/interfaces", "resolved": "packages/interfaces",
"link": true "link": true
}, },
"node_modules/@waku/message-encryption": {
"resolved": "packages/message-encryption",
"link": true
},
"node_modules/@waku/tests": { "node_modules/@waku/tests": {
"resolved": "packages/tests", "resolved": "packages/tests",
"link": true "link": true
@ -22506,12 +22510,11 @@
}, },
"packages/core": { "packages/core": {
"name": "@waku/core", "name": "@waku/core",
"version": "0.0.2", "version": "0.0.1",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@chainsafe/libp2p-gossipsub": "^4.1.1", "@chainsafe/libp2p-gossipsub": "^4.1.1",
"@chainsafe/libp2p-noise": "^8.0.1", "@chainsafe/libp2p-noise": "^8.0.1",
"@libp2p/crypto": "^1.0.4",
"@libp2p/interface-connection": "3.0.1", "@libp2p/interface-connection": "3.0.1",
"@libp2p/interface-peer-discovery": "^1.0.0", "@libp2p/interface-peer-discovery": "^1.0.0",
"@libp2p/interface-peer-id": "^1.0.2", "@libp2p/interface-peer-id": "^1.0.2",
@ -22523,14 +22526,12 @@
"@libp2p/peer-id": "^1.1.10", "@libp2p/peer-id": "^1.1.10",
"@libp2p/websockets": "^3.0.3", "@libp2p/websockets": "^3.0.3",
"@multiformats/multiaddr": "^11.0.6", "@multiformats/multiaddr": "^11.0.6",
"@noble/secp256k1": "^1.3.4", "@waku/byte-utils": "*",
"@waku/byte-utils": "0.0.1", "@waku/interfaces": "*",
"@waku/interfaces": "0.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"it-all": "^1.0.6", "it-all": "^1.0.6",
"it-length-prefixed": "^8.0.2", "it-length-prefixed": "^8.0.2",
"it-pipe": "^2.0.4", "it-pipe": "^2.0.4",
"js-sha3": "^0.8.0",
"libp2p": "0.38.0", "libp2p": "0.38.0",
"p-event": "^5.0.1", "p-event": "^5.0.1",
"protons-runtime": "^3.1.0", "protons-runtime": "^3.1.0",
@ -22904,6 +22905,41 @@
"npm": ">=7.0.0" "npm": ">=7.0.0"
} }
}, },
"packages/message-encryption": {
"name": "@waku/message-encryption",
"version": "0.0.1",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@noble/secp256k1": "^1.3.4",
"@waku/byte-utils": "*",
"@waku/interfaces": "*",
"js-sha3": "^0.8.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"chai": "^4.3.6",
"cspell": "^5.14.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"fast-check": "^2.14.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"rollup": "^2.75.0",
"ts-loader": "^9.4.1",
"typescript": "^4.6.3"
},
"engines": {
"node": ">=16"
}
},
"packages/tests": { "packages/tests": {
"name": "@waku/tests", "name": "@waku/tests",
"version": "0.0.1", "version": "0.0.1",
@ -27315,7 +27351,6 @@
"requires": { "requires": {
"@chainsafe/libp2p-gossipsub": "^4.1.1", "@chainsafe/libp2p-gossipsub": "^4.1.1",
"@chainsafe/libp2p-noise": "^8.0.1", "@chainsafe/libp2p-noise": "^8.0.1",
"@libp2p/crypto": "^1.0.4",
"@libp2p/interface-connection": "3.0.1", "@libp2p/interface-connection": "3.0.1",
"@libp2p/interface-peer-discovery": "^1.0.0", "@libp2p/interface-peer-discovery": "^1.0.0",
"@libp2p/interface-peer-id": "^1.0.2", "@libp2p/interface-peer-id": "^1.0.2",
@ -27327,7 +27362,6 @@
"@libp2p/peer-id": "^1.1.10", "@libp2p/peer-id": "^1.1.10",
"@libp2p/websockets": "^3.0.3", "@libp2p/websockets": "^3.0.3",
"@multiformats/multiaddr": "^11.0.6", "@multiformats/multiaddr": "^11.0.6",
"@noble/secp256k1": "^1.3.4",
"@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-node-resolve": "^13.3.0",
@ -27340,8 +27374,8 @@
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1", "@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1", "@typescript-eslint/parser": "^5.8.1",
"@waku/byte-utils": "0.0.1", "@waku/byte-utils": "*",
"@waku/interfaces": "0.0.1", "@waku/interfaces": "*",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"cspell": "^5.14.0", "cspell": "^5.14.0",
@ -27359,7 +27393,6 @@
"it-all": "^1.0.6", "it-all": "^1.0.6",
"it-length-prefixed": "^8.0.2", "it-length-prefixed": "^8.0.2",
"it-pipe": "^2.0.4", "it-pipe": "^2.0.4",
"js-sha3": "^0.8.0",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"karma": "^6.3.12", "karma": "^6.3.12",
@ -27631,6 +27664,34 @@
} }
} }
}, },
"@waku/message-encryption": {
"version": "file:packages/message-encryption",
"requires": {
"@noble/secp256k1": "^1.3.4",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@waku/byte-utils": "*",
"@waku/interfaces": "*",
"chai": "^4.3.6",
"cspell": "^5.14.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"fast-check": "^2.14.0",
"js-sha3": "^0.8.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"rollup": "^2.75.0",
"ts-loader": "^9.4.1",
"typescript": "^4.6.3"
}
},
"@waku/tests": { "@waku/tests": {
"version": "file:packages/tests", "version": "file:packages/tests",
"requires": { "requires": {

View File

@ -10,9 +10,10 @@ import debug from "debug";
import * as proto from "../../proto/message"; import * as proto from "../../proto/message";
const log = debug("waku:message:version-0"); const log = debug("waku:message:version-0");
const OneMillion = BigInt(1_000_000); const OneMillion = BigInt(1_000_000);
export const Version = 0; export const Version = 0;
export { proto };
export class MessageV0 implements Message { export class MessageV0 implements Message {
constructor(protected proto: proto.WakuMessage) {} constructor(protected proto: proto.WakuMessage) {}

View File

@ -53,8 +53,17 @@
"engines": { "engines": {
"node": ">=16" "node": ">=16"
}, },
"dependencies": {}, "browser": {
"crypto": false
},
"dependencies": {
"@noble/secp256k1": "^1.3.4",
"@waku/byte-utils": "*",
"@waku/interfaces": "*",
"js-sha3": "^0.8.0"
},
"devDependencies": { "devDependencies": {
"fast-check": "^2.14.0",
"@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-node-resolve": "^13.3.0",

View File

@ -0,0 +1,10 @@
export const Symmetric = {
keySize: 32,
ivSize: 12,
tagSize: 16,
algorithm: { name: "AES-GCM", length: 128 },
};
export const Asymmetric = {
keySize: 32,
};

View File

@ -0,0 +1,76 @@
import nodeCrypto from "crypto";
import * as secp from "@noble/secp256k1";
import { concat } from "@waku/byte-utils";
import sha3 from "js-sha3";
import { Asymmetric, Symmetric } from "./constants.js";
declare const self: Record<string, any> | undefined;
const crypto: { node?: any; web?: any } = {
node: nodeCrypto,
web: typeof self === "object" && "crypto" in self ? self.crypto : undefined,
};
export function getSubtle(): SubtleCrypto {
if (crypto.web) {
return crypto.web.subtle;
} else if (crypto.node) {
return crypto.node.webcrypto.subtle;
} else {
throw new Error(
"The environment doesn't have Crypto Subtle API (if in the browser, be sure to use to be in a secure context, ie, https)"
);
}
}
export const randomBytes = secp.utils.randomBytes;
export const sha256 = secp.utils.sha256;
/**
* Generate a new private key to be used for asymmetric encryption.
*
* Use {@link getPublicKey} to get the corresponding Public Key.
*/
export function generatePrivateKey(): Uint8Array {
return randomBytes(Asymmetric.keySize);
}
/**
* Generate a new symmetric key to be used for symmetric encryption.
*/
export function generateSymmetricKey(): Uint8Array {
return randomBytes(Symmetric.keySize);
}
/**
* Return the public key for the given private key, to be used for asymmetric
* encryption.
*/
export const getPublicKey = secp.getPublicKey;
/**
* ECDSA Sign a message with the given private key.
*
* @param message The message to sign, usually a hash.
* @param privateKey The ECDSA private key to use to sign the message.
*
* @returns The signature and the recovery id concatenated.
*/
export async function sign(
message: Uint8Array,
privateKey: Uint8Array
): Promise<Uint8Array> {
const [signature, recoveryId] = await secp.sign(message, privateKey, {
recovered: true,
der: false,
});
return concat(
[signature, new Uint8Array([recoveryId])],
signature.length + 1
);
}
export function keccak256(input: Uint8Array): Uint8Array {
return new Uint8Array(sha3.keccak256.arrayBuffer(input));
}

View File

@ -0,0 +1,194 @@
import * as secp from "@noble/secp256k1";
import { concat, hexToBytes } from "@waku/byte-utils";
import { getSubtle, randomBytes, sha256 } from "./crypto.js";
/**
* HKDF as implemented in go-ethereum.
*/
function kdf(secret: Uint8Array, outputLength: number): Promise<Uint8Array> {
let ctr = 1;
let written = 0;
let willBeResult = Promise.resolve(new Uint8Array());
while (written < outputLength) {
const counters = new Uint8Array([ctr >> 24, ctr >> 16, ctr >> 8, ctr]);
const countersSecret = concat(
[counters, secret],
counters.length + secret.length
);
const willBeHashResult = sha256(countersSecret);
willBeResult = willBeResult.then((result) =>
willBeHashResult.then((hashResult) => {
const _hashResult = new Uint8Array(hashResult);
return concat(
[result, _hashResult],
result.length + _hashResult.length
);
})
);
written += 32;
ctr += 1;
}
return willBeResult;
}
function aesCtrEncrypt(
counter: Uint8Array,
key: ArrayBufferLike,
data: ArrayBufferLike
): Promise<Uint8Array> {
return getSubtle()
.importKey("raw", key, "AES-CTR", false, ["encrypt"])
.then((cryptoKey) =>
getSubtle().encrypt(
{ name: "AES-CTR", counter: counter, length: 128 },
cryptoKey,
data
)
)
.then((bytes) => new Uint8Array(bytes));
}
function aesCtrDecrypt(
counter: Uint8Array,
key: ArrayBufferLike,
data: ArrayBufferLike
): Promise<Uint8Array> {
return getSubtle()
.importKey("raw", key, "AES-CTR", false, ["decrypt"])
.then((cryptoKey) =>
getSubtle().decrypt(
{ name: "AES-CTR", counter: counter, length: 128 },
cryptoKey,
data
)
)
.then((bytes) => new Uint8Array(bytes));
}
function hmacSha256Sign(
key: ArrayBufferLike,
msg: ArrayBufferLike
): PromiseLike<Uint8Array> {
const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
return getSubtle()
.importKey("raw", key, algorithm, false, ["sign"])
.then((cryptoKey) => getSubtle().sign(algorithm, cryptoKey, msg))
.then((bytes) => new Uint8Array(bytes));
}
function hmacSha256Verify(
key: ArrayBufferLike,
msg: ArrayBufferLike,
sig: ArrayBufferLike
): Promise<boolean> {
const algorithm = { name: "HMAC", hash: { name: "SHA-256" } };
const _key = getSubtle().importKey("raw", key, algorithm, false, ["verify"]);
return _key.then((cryptoKey) =>
getSubtle().verify(algorithm, cryptoKey, sig, msg)
);
}
/**
* Derive shared secret for given private and public keys.
*
* @param privateKeyA Sender's private key (32 bytes)
* @param publicKeyB Recipient's public key (65 bytes)
* @returns A promise that resolves with the derived shared secret (Px, 32 bytes)
* @throws Error If arguments are invalid
*/
function derive(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array {
if (privateKeyA.length !== 32) {
throw new Error(
`Bad private key, it should be 32 bytes but it's actually ${privateKeyA.length} bytes long`
);
} else if (publicKeyB.length !== 65) {
throw new Error(
`Bad public key, it should be 65 bytes but it's actually ${publicKeyB.length} bytes long`
);
} else if (publicKeyB[0] !== 4) {
throw new Error("Bad public key, a valid public key would begin with 4");
} else {
const px = secp.getSharedSecret(privateKeyA, publicKeyB, true);
// Remove the compression prefix
return new Uint8Array(hexToBytes(px).slice(1));
}
}
/**
* Encrypt message for given recipient's public key.
*
* @param publicKeyTo Recipient's public key (65 bytes)
* @param msg The message being encrypted
* @return A promise that resolves with the ECIES structure serialized
*/
export async function encrypt(
publicKeyTo: Uint8Array,
msg: Uint8Array
): Promise<Uint8Array> {
const ephemPrivateKey = randomBytes(32);
const sharedPx = await derive(ephemPrivateKey, publicKeyTo);
const hash = await kdf(sharedPx, 32);
const iv = randomBytes(16);
const encryptionKey = hash.slice(0, 16);
const cipherText = await aesCtrEncrypt(iv, encryptionKey, msg);
const ivCipherText = concat([iv, cipherText], iv.length + cipherText.length);
const macKey = await sha256(hash.slice(16));
const hmac = await hmacSha256Sign(macKey, ivCipherText);
const ephemPublicKey = secp.getPublicKey(ephemPrivateKey, false);
return concat(
[ephemPublicKey, ivCipherText, hmac],
ephemPublicKey.length + ivCipherText.length + hmac.length
);
}
const metaLength = 1 + 64 + 16 + 32;
/**
* Decrypt message using given private key.
*
* @param privateKey A 32-byte private key of recipient of the message
* @param encrypted ECIES serialized structure (result of ECIES encryption)
* @returns The clear text
* @throws Error If decryption fails
*/
export async function decrypt(
privateKey: Uint8Array,
encrypted: Uint8Array
): Promise<Uint8Array> {
if (encrypted.length <= metaLength) {
throw new Error(
`Invalid Ciphertext. Data is too small. It should ba at least ${metaLength} bytes`
);
} else if (encrypted[0] !== 4) {
throw new Error(
`Not a valid ciphertext. It should begin with 4 but actually begin with ${encrypted[0]}`
);
} else {
// deserialize
const ephemPublicKey = encrypted.slice(0, 65);
const cipherTextLength = encrypted.length - metaLength;
const iv = encrypted.slice(65, 65 + 16);
const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength);
const ciphertext = cipherAndIv.slice(16);
const msgMac = encrypted.slice(65 + 16 + cipherTextLength);
// check HMAC
const px = derive(privateKey, ephemPublicKey);
const hash = await kdf(px, 32);
const [encryptionKey, macKey] = await sha256(hash.slice(16)).then(
(macKey) => [hash.slice(0, 16), macKey]
);
if (!(await hmacSha256Verify(macKey, cipherAndIv, msgMac))) {
throw new Error("Incorrect MAC");
}
return aesCtrDecrypt(iv, encryptionKey, ciphertext);
}
}

View File

@ -0,0 +1,208 @@
import { expect } from "chai";
import fc from "fast-check";
import { getPublicKey } from "./crypto.js";
import {
AsymDecoder,
AsymEncoder,
decryptAsymmetric,
decryptSymmetric,
encryptAsymmetric,
encryptSymmetric,
postCipher,
preCipher,
SymDecoder,
SymEncoder,
} from "./index.js";
const TestContentTopic = "/test/1/waku-message/utf8";
describe("Waku Message version 1", function () {
it("Round trip binary encryption [asymmetric, no signature]", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (payload, privateKey) => {
const publicKey = getPublicKey(privateKey);
const encoder = new AsymEncoder(TestContentTopic, publicKey);
const bytes = await encoder.toWire({ payload });
const decoder = new AsymDecoder(TestContentTopic, privateKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(protoResult);
if (!result) throw "Failed to decode";
expect(result.contentTopic).to.equal(TestContentTopic);
expect(result.version).to.equal(1);
expect(result?.payload).to.deep.equal(payload);
expect(result.signature).to.be.undefined;
expect(result.signaturePublicKey).to.be.undefined;
}
)
);
});
it("R trip binary encryption [asymmetric, signature]", async function () {
this.timeout(4000);
await fc.assert(
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (payload, alicePrivateKey, bobPrivateKey) => {
const alicePublicKey = getPublicKey(alicePrivateKey);
const bobPublicKey = getPublicKey(bobPrivateKey);
const encoder = new AsymEncoder(
TestContentTopic,
bobPublicKey,
alicePrivateKey
);
const bytes = await encoder.toWire({ payload });
const decoder = new AsymDecoder(TestContentTopic, bobPrivateKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(protoResult);
if (!result) throw "Failed to decode";
expect(result.contentTopic).to.equal(TestContentTopic);
expect(result.version).to.equal(1);
expect(result?.payload).to.deep.equal(payload);
expect(result.signature).to.not.be.undefined;
expect(result.signaturePublicKey).to.deep.eq(alicePublicKey);
}
)
);
});
it("Round trip binary encryption [symmetric, no signature]", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (payload, symKey) => {
const encoder = new SymEncoder(TestContentTopic, symKey);
const bytes = await encoder.toWire({ payload });
const decoder = new SymDecoder(TestContentTopic, symKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(protoResult);
if (!result) throw "Failed to decode";
expect(result.contentTopic).to.equal(TestContentTopic);
expect(result.version).to.equal(1);
expect(result?.payload).to.deep.equal(payload);
expect(result.signature).to.be.undefined;
expect(result.signaturePublicKey).to.be.undefined;
}
)
);
});
it("Round trip binary encryption [symmetric, signature]", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (payload, sigPrivKey, symKey) => {
const sigPubKey = getPublicKey(sigPrivKey);
const encoder = new SymEncoder(TestContentTopic, symKey, sigPrivKey);
const bytes = await encoder.toWire({ payload });
const decoder = new SymDecoder(TestContentTopic, symKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(protoResult);
if (!result) throw "Failed to decode";
expect(result.contentTopic).to.equal(TestContentTopic);
expect(result.version).to.equal(1);
expect(result?.payload).to.deep.equal(payload);
expect(result.signature).to.not.be.undefined;
expect(result.signaturePublicKey).to.deep.eq(sigPubKey);
}
)
);
});
});
describe("Encryption helpers", () => {
it("Asymmetric encrypt & decrypt", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (message, privKey) => {
const publicKey = getPublicKey(privKey);
const enc = await encryptAsymmetric(message, publicKey);
const res = await decryptAsymmetric(enc, privKey);
expect(res).deep.equal(message);
}
)
);
});
it("Symmetric encrypt & Decrypt", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array(),
fc.uint8Array({ minLength: 32, maxLength: 32 }),
async (message, key) => {
const enc = await encryptSymmetric(message, key);
const res = await decryptSymmetric(enc, key);
expect(res).deep.equal(message);
}
)
);
});
it("pre and post cipher", async function () {
await fc.assert(
fc.asyncProperty(fc.uint8Array(), async (message) => {
const enc = await preCipher(message);
const res = postCipher(enc);
expect(res?.payload).deep.equal(
message,
"Payload was not encrypted then decrypted correctly"
);
})
);
});
it("Sign & Recover", async function () {
await fc.assert(
fc.asyncProperty(
fc.uint8Array(),
fc.uint8Array({ minLength: 32, maxLength: 32 }),
async (message, sigPrivKey) => {
const sigPubKey = getPublicKey(sigPrivKey);
const enc = await preCipher(message, sigPrivKey);
const res = postCipher(enc);
expect(res?.payload).deep.equal(
message,
"Payload was not encrypted then decrypted correctly"
);
expect(res?.sig?.publicKey).deep.equal(
sigPubKey,
"signature Public key was not recovered from encrypted then decrypted signature"
);
}
)
);
});
});

View File

@ -0,0 +1,459 @@
import * as secp from "@noble/secp256k1";
import { concat, hexToBytes } from "@waku/byte-utils";
import {
DecoderV0,
MessageV0,
proto,
} from "@waku/core/lib/waku_message/version_0";
import type { Decoder, Encoder, Message, ProtoMessage } from "@waku/interfaces";
import debug from "debug";
import { Symmetric } from "./constants.js";
import { keccak256, randomBytes, sign } from "./crypto.js";
import * as ecies from "./ecies.js";
import * as symmetric from "./symmetric.js";
const log = debug("waku:message:version-1");
const FlagsLength = 1;
const FlagMask = 3; // 0011
const IsSignedMask = 4; // 0100
const PaddingTarget = 256;
const SignatureLength = 65;
const OneMillion = BigInt(1_000_000);
export const Version = 1;
export type Signature = {
signature: Uint8Array;
publicKey: Uint8Array | undefined;
};
export class MessageV1 extends MessageV0 implements Message {
private readonly _decodedPayload: Uint8Array;
constructor(
proto: proto.WakuMessage,
decodedPayload: Uint8Array,
public signature?: Uint8Array,
public signaturePublicKey?: Uint8Array
) {
super(proto);
this._decodedPayload = decodedPayload;
}
get payload(): Uint8Array {
return this._decodedPayload;
}
}
export class AsymEncoder implements Encoder {
constructor(
public contentTopic: string,
private publicKey: Uint8Array,
private sigPrivKey?: Uint8Array
) {}
async toWire(message: Partial<Message>): Promise<Uint8Array | undefined> {
const protoMessage = await this.toProtoObj(message);
if (!protoMessage) return;
return proto.WakuMessage.encode(protoMessage);
}
async toProtoObj(
message: Partial<Message>
): Promise<ProtoMessage | undefined> {
const timestamp = message.timestamp ?? new Date();
if (!message.payload) {
log("No payload to encrypt, skipping: ", message);
return;
}
const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
const payload = await encryptAsymmetric(preparedPayload, this.publicKey);
return {
payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
rateLimitProof: message.rateLimitProof,
};
}
}
export class SymEncoder implements Encoder {
constructor(
public contentTopic: string,
private symKey: Uint8Array,
private sigPrivKey?: Uint8Array
) {}
async toWire(message: Partial<Message>): Promise<Uint8Array | undefined> {
const protoMessage = await this.toProtoObj(message);
if (!protoMessage) return;
return proto.WakuMessage.encode(protoMessage);
}
async toProtoObj(
message: Partial<Message>
): Promise<ProtoMessage | undefined> {
const timestamp = message.timestamp ?? new Date();
if (!message.payload) {
log("No payload to encrypt, skipping: ", message);
return;
}
const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
const payload = await encryptSymmetric(preparedPayload, this.symKey);
return {
payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
rateLimitProof: message.rateLimitProof,
};
}
}
export class AsymDecoder extends DecoderV0 implements Decoder<MessageV1> {
constructor(contentTopic: string, private privateKey: Uint8Array) {
super(contentTopic);
}
async fromProtoObj(
protoMessage: ProtoMessage
): Promise<MessageV1 | undefined> {
const cipherPayload = protoMessage.payload;
if (protoMessage.version !== Version) {
log(
"Failed to decrypt due to incorrect version, expected:",
Version,
", actual:",
protoMessage.version
);
return;
}
let payload;
if (!cipherPayload) {
log(`No payload to decrypt for contentTopic ${this.contentTopic}`);
return;
}
try {
payload = await decryptAsymmetric(cipherPayload, this.privateKey);
} catch (e) {
log(
`Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`,
e
);
return;
}
if (!payload) {
log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`);
return;
}
const res = await postCipher(payload);
if (!res) {
log(`Failed to decode payload for contentTopic ${this.contentTopic}`);
return;
}
log("Message decrypted", protoMessage);
return new MessageV1(
protoMessage,
res.payload,
res.sig?.signature,
res.sig?.publicKey
);
}
}
export class SymDecoder extends DecoderV0 implements Decoder<MessageV1> {
constructor(contentTopic: string, private symKey: Uint8Array) {
super(contentTopic);
}
async fromProtoObj(
protoMessage: ProtoMessage
): Promise<MessageV1 | undefined> {
const cipherPayload = protoMessage.payload;
if (protoMessage.version !== Version) {
log(
"Failed to decrypt due to incorrect version, expected:",
Version,
", actual:",
protoMessage.version
);
return;
}
let payload;
if (!cipherPayload) {
log(`No payload to decrypt for contentTopic ${this.contentTopic}`);
return;
}
try {
payload = await decryptSymmetric(cipherPayload, this.symKey);
} catch (e) {
log(
`Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`,
e
);
return;
}
if (!payload) {
log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`);
return;
}
const res = await postCipher(payload);
if (!res) {
log(`Failed to decode payload for contentTopic ${this.contentTopic}`);
return;
}
log("Message decrypted", protoMessage);
return new MessageV1(
protoMessage,
res.payload,
res.sig?.signature,
res.sig?.publicKey
);
}
}
function getSizeOfPayloadSizeField(message: Uint8Array): number {
const messageDataView = new DataView(message.buffer);
return messageDataView.getUint8(0) & FlagMask;
}
function getPayloadSize(
message: Uint8Array,
sizeOfPayloadSizeField: number
): number {
let payloadSizeBytes = message.slice(1, 1 + sizeOfPayloadSizeField);
// int 32 == 4 bytes
if (sizeOfPayloadSizeField < 4) {
// If less than 4 bytes pad right (Little Endian).
payloadSizeBytes = concat(
[payloadSizeBytes, new Uint8Array(4 - sizeOfPayloadSizeField)],
4
);
}
const payloadSizeDataView = new DataView(payloadSizeBytes.buffer);
return payloadSizeDataView.getInt32(0, true);
}
function isMessageSigned(message: Uint8Array): boolean {
const messageDataView = new DataView(message.buffer);
return (messageDataView.getUint8(0) & IsSignedMask) == IsSignedMask;
}
/**
* Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
* The data MUST be flags | payload-length | payload | [signature].
* The returned result can be set to `WakuMessage.payload`.
*
* @internal
*/
export async function encryptAsymmetric(
data: Uint8Array,
publicKey: Uint8Array | string
): Promise<Uint8Array> {
return ecies.encrypt(hexToBytes(publicKey), data);
}
/**
* Proceed with Asymmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
* The returned data is expected to be `flags | payload-length | payload | [signature]`.
*
* @internal
*/
export async function decryptAsymmetric(
payload: Uint8Array,
privKey: Uint8Array
): Promise<Uint8Array> {
return ecies.decrypt(privKey, payload);
}
/**
* Proceed with Symmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
*
* @param data The data to encrypt, expected to be `flags | payload-length | payload | [signature]`.
* @param key The key to use for encryption.
* @returns The decrypted data, `cipherText | tag | iv` and can be set to `WakuMessage.payload`.
*
* @internal
*/
export async function encryptSymmetric(
data: Uint8Array,
key: Uint8Array | string
): Promise<Uint8Array> {
const iv = symmetric.generateIv();
// Returns `cipher | tag`
const cipher = await symmetric.encrypt(iv, hexToBytes(key), data);
return concat([cipher, iv]);
}
/**
* Proceed with Symmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
*
* @param payload The cipher data, it is expected to be `cipherText | tag | iv`.
* @param key The key to use for decryption.
* @returns The decrypted data, expected to be `flags | payload-length | payload | [signature]`.
*
* @internal
*/
export async function decryptSymmetric(
payload: Uint8Array,
key: Uint8Array | string
): Promise<Uint8Array> {
const ivStart = payload.length - Symmetric.ivSize;
const cipher = payload.slice(0, ivStart);
const iv = payload.slice(ivStart);
return symmetric.decrypt(iv, hexToBytes(key), cipher);
}
/**
* Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/).
*/
function addPayloadSizeField(msg: Uint8Array, payload: Uint8Array): Uint8Array {
const fieldSize = computeSizeOfPayloadSizeField(payload);
let field = new Uint8Array(4);
const fieldDataView = new DataView(field.buffer);
fieldDataView.setUint32(0, payload.length, true);
field = field.slice(0, fieldSize);
msg = concat([msg, field]);
msg[0] |= fieldSize;
return msg;
}
/**
* Returns the size of the auxiliary-field which in turns contains the payload size
*/
function computeSizeOfPayloadSizeField(payload: Uint8Array): number {
let s = 1;
for (let i = payload.length; i >= 256; i /= 256) {
s++;
}
return s;
}
function validateDataIntegrity(
value: Uint8Array,
expectedSize: number
): boolean {
if (value.length !== expectedSize) {
return false;
}
return expectedSize <= 3 || value.findIndex((i) => i !== 0) !== -1;
}
function getSignature(message: Uint8Array): Uint8Array {
return message.slice(message.length - SignatureLength, message.length);
}
function getHash(message: Uint8Array, isSigned: boolean): Uint8Array {
if (isSigned) {
return keccak256(message.slice(0, message.length - SignatureLength));
}
return keccak256(message);
}
function ecRecoverPubKey(
messageHash: Uint8Array,
signature: Uint8Array
): Uint8Array | undefined {
const recoveryDataView = new DataView(signature.slice(64).buffer);
const recovery = recoveryDataView.getUint8(0);
const _signature = secp.Signature.fromCompact(signature.slice(0, 64));
return secp.recoverPublicKey(messageHash, _signature, recovery, false);
}
/**
* Prepare the payload pre-encryption.
*
* @internal
* @returns The encoded payload, ready for encryption using {@link encryptAsymmetric}
* or {@link encryptSymmetric}.
*/
export async function preCipher(
messagePayload: Uint8Array,
sigPrivKey?: Uint8Array
): Promise<Uint8Array> {
let envelope = new Uint8Array([0]); // No flags
envelope = addPayloadSizeField(envelope, messagePayload);
envelope = concat([envelope, messagePayload]);
// Calculate padding:
let rawSize =
FlagsLength +
computeSizeOfPayloadSizeField(messagePayload) +
messagePayload.length;
if (sigPrivKey) {
rawSize += SignatureLength;
}
const remainder = rawSize % PaddingTarget;
const paddingSize = PaddingTarget - remainder;
const pad = randomBytes(paddingSize);
if (!validateDataIntegrity(pad, paddingSize)) {
throw new Error("failed to generate random padding of size " + paddingSize);
}
envelope = concat([envelope, pad]);
if (sigPrivKey) {
envelope[0] |= IsSignedMask;
const hash = keccak256(envelope);
const bytesSignature = await sign(hash, sigPrivKey);
envelope = concat([envelope, bytesSignature]);
}
return envelope;
}
/**
* Decode a decrypted payload.
*
* @internal
*/
export function postCipher(
message: Uint8Array
): { payload: Uint8Array; sig?: Signature } | undefined {
const sizeOfPayloadSizeField = getSizeOfPayloadSizeField(message);
if (sizeOfPayloadSizeField === 0) return;
const payloadSize = getPayloadSize(message, sizeOfPayloadSizeField);
const payloadStart = 1 + sizeOfPayloadSizeField;
const payload = message.slice(payloadStart, payloadStart + payloadSize);
const isSigned = isMessageSigned(message);
let sig;
if (isSigned) {
const signature = getSignature(message);
const hash = getHash(message, isSigned);
const publicKey = ecRecoverPubKey(hash, signature);
sig = { signature, publicKey };
}
return { payload, sig };
}

View File

@ -0,0 +1,32 @@
import { Symmetric } from "./constants";
import { getSubtle, randomBytes } from "./crypto.js";
export async function encrypt(
iv: Uint8Array,
key: Uint8Array,
clearText: Uint8Array
): Promise<Uint8Array> {
return getSubtle()
.importKey("raw", key, Symmetric.algorithm, false, ["encrypt"])
.then((cryptoKey) =>
getSubtle().encrypt({ iv, ...Symmetric.algorithm }, cryptoKey, clearText)
)
.then((cipher) => new Uint8Array(cipher));
}
export async function decrypt(
iv: Uint8Array,
key: Uint8Array,
cipherText: Uint8Array
): Promise<Uint8Array> {
return getSubtle()
.importKey("raw", key, Symmetric.algorithm, false, ["decrypt"])
.then((cryptoKey) =>
getSubtle().decrypt({ iv, ...Symmetric.algorithm }, cryptoKey, cipherText)
)
.then((clear) => new Uint8Array(clear));
}
export function generateIv(): Uint8Array {
return randomBytes(Symmetric.ivSize);
}