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",
|
"types": "./dist/lib/waku_message/version_0.d.ts",
|
||||||
"import": "./dist/lib/waku_message/version_0.js"
|
"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": {
|
"./lib/waku_message/topic_only_message": {
|
||||||
"types": "./dist/lib/waku_message/topic_only_message.d.ts",
|
"types": "./dist/lib/waku_message/topic_only_message.d.ts",
|
||||||
"import": "./dist/lib/waku_message/topic_only_message.js"
|
"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",
|
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build",
|
||||||
"release": "semantic-release"
|
"release": "semantic-release"
|
||||||
},
|
},
|
||||||
"browser": {
|
|
||||||
"crypto": false
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
|
@ -97,7 +90,6 @@
|
||||||
"@waku/byte-utils": "*",
|
"@waku/byte-utils": "*",
|
||||||
"@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",
|
||||||
|
@ -109,13 +101,11 @@
|
||||||
"@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/interfaces": "*",
|
"@waku/interfaces": "*",
|
||||||
"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",
|
||||||
|
|
|
@ -9,7 +9,6 @@ export default {
|
||||||
"lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js",
|
"lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js",
|
||||||
"lib/wait_for_remote_peer": "dist/lib/wait_for_remote_peer.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_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":
|
"lib/waku_message/topic_only_message":
|
||||||
"dist/lib/waku_message/topic_only_message.js",
|
"dist/lib/waku_message/topic_only_message.js",
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
export { DefaultPubSubTopic } from "./lib/constants";
|
export { DefaultPubSubTopic } from "./lib/constants";
|
||||||
|
|
||||||
export {
|
|
||||||
generatePrivateKey,
|
|
||||||
generateSymmetricKey,
|
|
||||||
getPublicKey,
|
|
||||||
} from "./lib/crypto";
|
|
||||||
|
|
||||||
export * as proto_message from "./proto/message";
|
export * as proto_message from "./proto/message";
|
||||||
export * as proto_topic_only_message from "./proto/topic_only_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