mirror of https://github.com/waku-org/js-waku.git
chore: make message-encryption compile
This commit is contained in:
parent
256b7223f3
commit
e6efd0438c
|
@ -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": {
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const Symmetric = {
|
||||||
|
keySize: 32,
|
||||||
|
ivSize: 12,
|
||||||
|
tagSize: 16,
|
||||||
|
algorithm: { name: "AES-GCM", length: 128 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Asymmetric = {
|
||||||
|
keySize: 32,
|
||||||
|
};
|
|
@ -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));
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 };
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in New Issue