mirror of https://github.com/waku-org/js-waku.git
chore!: extract version-1 from chore
This commit is contained in:
parent
a20b7809d6
commit
256b7223f3
|
@ -25,10 +25,6 @@
|
|||
"types": "./dist/lib/waku_message/version_0.d.ts",
|
||||
"import": "./dist/lib/waku_message/version_0.js"
|
||||
},
|
||||
"./lib/waku_message/version_1": {
|
||||
"types": "./dist/lib/waku_message/version_1.d.ts",
|
||||
"import": "./dist/lib/waku_message/version_1.js"
|
||||
},
|
||||
"./lib/waku_message/topic_only_message": {
|
||||
"types": "./dist/lib/waku_message/topic_only_message.d.ts",
|
||||
"import": "./dist/lib/waku_message/topic_only_message.js"
|
||||
|
@ -87,9 +83,6 @@
|
|||
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build",
|
||||
"release": "semantic-release"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
|
@ -97,7 +90,6 @@
|
|||
"@waku/byte-utils": "*",
|
||||
"@chainsafe/libp2p-gossipsub": "^4.1.1",
|
||||
"@chainsafe/libp2p-noise": "^8.0.1",
|
||||
"@libp2p/crypto": "^1.0.4",
|
||||
"@libp2p/interface-connection": "3.0.1",
|
||||
"@libp2p/interface-peer-discovery": "^1.0.0",
|
||||
"@libp2p/interface-peer-id": "^1.0.2",
|
||||
|
@ -109,13 +101,11 @@
|
|||
"@libp2p/peer-id": "^1.1.10",
|
||||
"@libp2p/websockets": "^3.0.3",
|
||||
"@multiformats/multiaddr": "^11.0.6",
|
||||
"@noble/secp256k1": "^1.3.4",
|
||||
"@waku/interfaces": "*",
|
||||
"debug": "^4.3.4",
|
||||
"it-all": "^1.0.6",
|
||||
"it-length-prefixed": "^8.0.2",
|
||||
"it-pipe": "^2.0.4",
|
||||
"js-sha3": "^0.8.0",
|
||||
"libp2p": "0.38.0",
|
||||
"p-event": "^5.0.1",
|
||||
"protons-runtime": "^3.1.0",
|
||||
|
|
|
@ -9,7 +9,6 @@ export default {
|
|||
"lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js",
|
||||
"lib/wait_for_remote_peer": "dist/lib/wait_for_remote_peer.js",
|
||||
"lib/waku_message/version_0": "dist/lib/waku_message/version_0.js",
|
||||
"lib/waku_message/version_1": "dist/lib/waku_message/version_1.js",
|
||||
"lib/waku_message/topic_only_message":
|
||||
"dist/lib/waku_message/topic_only_message.js",
|
||||
},
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
export { DefaultPubSubTopic } from "./lib/constants";
|
||||
|
||||
export {
|
||||
generatePrivateKey,
|
||||
generateSymmetricKey,
|
||||
getPublicKey,
|
||||
} from "./lib/crypto";
|
||||
|
||||
export * as proto_message from "./proto/message";
|
||||
export * as proto_topic_only_message from "./proto/topic_only_message";
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
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 "./waku_message/constants";
|
||||
|
||||
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));
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
export const Symmetric = {
|
||||
keySize: 32,
|
||||
ivSize: 12,
|
||||
tagSize: 16,
|
||||
algorithm: { name: "AES-GCM", length: 128 },
|
||||
};
|
||||
|
||||
export const Asymmetric = {
|
||||
keySize: 32,
|
||||
};
|
|
@ -1,194 +0,0 @@
|
|||
import * as secp from "@noble/secp256k1";
|
||||
import { concat, hexToBytes } from "@waku/byte-utils";
|
||||
|
||||
import { getSubtle, randomBytes, sha256 } from "../crypto";
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
import { getSubtle, randomBytes } from "../crypto";
|
||||
|
||||
import { Symmetric } from "./constants";
|
||||
|
||||
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);
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
import { expect } from "chai";
|
||||
import fc from "fast-check";
|
||||
|
||||
import { getPublicKey } from "../crypto";
|
||||
|
||||
import {
|
||||
AsymDecoder,
|
||||
AsymEncoder,
|
||||
decryptAsymmetric,
|
||||
decryptSymmetric,
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
postCipher,
|
||||
preCipher,
|
||||
SymDecoder,
|
||||
SymEncoder,
|
||||
} from "./version_1";
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,457 +0,0 @@
|
|||
import * as secp from "@noble/secp256k1";
|
||||
import { concat, hexToBytes } from "@waku/byte-utils";
|
||||
import type { Decoder, Encoder, Message, ProtoMessage } from "@waku/interfaces";
|
||||
import debug from "debug";
|
||||
|
||||
import * as proto from "../../proto/message";
|
||||
import { keccak256, randomBytes, sign } from "../crypto";
|
||||
|
||||
import { Symmetric } from "./constants";
|
||||
import * as ecies from "./ecies";
|
||||
import * as symmetric from "./symmetric";
|
||||
import { DecoderV0, MessageV0 } from "./version_0";
|
||||
|
||||
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 };
|
||||
}
|
Loading…
Reference in New Issue