diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9b5ae0ed..a98be26bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handle errors thrown by `bytesToUtf8`. +### Removed + +- Removed `ecies-geth` dependency. + ## [0.18.0] - 2022-02-24 ### Changed diff --git a/package-lock.json b/package-lock.json index 4c92364c40..ea38e3d3f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@ethersproject/rlp": "^5.5.0", "debug": "^4.3.1", "dns-query": "^0.8.0", - "ecies-geth": "^1.5.2", "hi-base32": "^0.5.1", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.2", @@ -4319,15 +4318,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "node_modules/ecies-geth": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ecies-geth/-/ecies-geth-1.6.0.tgz", - "integrity": "sha512-lTfWFECCaxxtSinZIYRy/HxovXlEHdA6Y2K6qwsdg/l9y2CnZZopKQFQzvnGUUMMBMe0JIlNVYSKbLXpV+Tubw==", - "dependencies": { - "elliptic": "^6.5.4", - "secp256k1": "^4.0.2" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -16178,15 +16168,6 @@ } } }, - "ecies-geth": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ecies-geth/-/ecies-geth-1.6.0.tgz", - "integrity": "sha512-lTfWFECCaxxtSinZIYRy/HxovXlEHdA6Y2K6qwsdg/l9y2CnZZopKQFQzvnGUUMMBMe0JIlNVYSKbLXpV+Tubw==", - "requires": { - "elliptic": "^6.5.4", - "secp256k1": "^4.0.2" - } - }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", diff --git a/package.json b/package.json index df027c7ab9..f77ab79134 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@ethersproject/rlp": "^5.5.0", "debug": "^4.3.1", "dns-query": "^0.8.0", - "ecies-geth": "^1.5.2", "hi-base32": "^0.5.1", "it-concat": "^2.0.0", "it-length-prefixed": "^5.0.2", diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts new file mode 100644 index 0000000000..87011c3d99 --- /dev/null +++ b/src/lib/crypto.ts @@ -0,0 +1,29 @@ +import nodeCrypto from "crypto"; + +// IE 11 +declare global { + interface Window { + msCrypto?: Crypto; + } + + interface Crypto { + webkitSubtle?: SubtleCrypto; + } +} + +const crypto = window.crypto || window.msCrypto || nodeCrypto.webcrypto; +const subtle: SubtleCrypto = crypto.subtle || crypto.webkitSubtle; + +if (subtle === undefined) { + throw new Error("crypto and/or subtle api unavailable"); +} + +export { crypto, subtle }; + +export function randomBytes(size: number): Uint8Array { + return crypto.getRandomValues(new Uint8Array(size)); +} + +export function sha256(msg: ArrayBufferLike): Promise { + return subtle.digest({ name: "SHA-256" }, msg); +} diff --git a/src/lib/waku_message/ecies.ts b/src/lib/waku_message/ecies.ts new file mode 100644 index 0000000000..a4540b26d7 --- /dev/null +++ b/src/lib/waku_message/ecies.ts @@ -0,0 +1,202 @@ +import * as secp from "@noble/secp256k1"; + +import { randomBytes, sha256, subtle } from "../crypto"; +import { hexToBytes } from "../utils"; + +/** + * HKDF as implemented in go-ethereum. + */ +function kdf(secret: Uint8Array, outputLength: number): Promise { + 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 = new Uint8Array(counters.length + secret.length); + countersSecret.set(counters, 0); + countersSecret.set(secret, counters.length); + const willBeHashResult = sha256(countersSecret); + willBeResult = willBeResult.then((result) => + willBeHashResult.then((hashResult) => { + const _hashResult = new Uint8Array(hashResult); + const _res = new Uint8Array(result.length + _hashResult.length); + _res.set(result, 0); + _res.set(_hashResult, result.length); + return _res; + }) + ); + written += 32; + ctr += 1; + } + return willBeResult; +} + +function aesCtrEncrypt( + counter: Uint8Array, + key: ArrayBufferLike, + data: ArrayBufferLike +): Promise { + return subtle + .importKey("raw", key, "AES-CTR", false, ["encrypt"]) + .then((cryptoKey) => + subtle.encrypt( + { name: "AES-CTR", counter: counter, length: 128 }, + cryptoKey, + data + ) + ) + .then((bytes) => new Uint8Array(bytes)); +} + +function aesCtrDecrypt( + counter: Uint8Array, + key: ArrayBufferLike, + data: ArrayBufferLike +): Promise { + return subtle + .importKey("raw", key, "AES-CTR", false, ["decrypt"]) + .then((cryptoKey) => + subtle.decrypt( + { name: "AES-CTR", counter: counter, length: 128 }, + cryptoKey, + data + ) + ) + .then((bytes) => new Uint8Array(bytes)); +} + +function hmacSha256Sign( + key: ArrayBufferLike, + msg: ArrayBufferLike +): PromiseLike { + const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; + return subtle + .importKey("raw", key, algorithm, false, ["sign"]) + .then((cryptoKey) => subtle.sign(algorithm, cryptoKey, msg)) + .then((bytes) => new Uint8Array(bytes)); +} + +function hmacSha256Verify( + key: ArrayBufferLike, + msg: ArrayBufferLike, + sig: ArrayBufferLike +): Promise { + const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; + const _key = subtle.importKey("raw", key, algorithm, false, ["verify"]); + return _key.then((cryptoKey) => + subtle.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 { + 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 = new Uint8Array(iv.length + cipherText.length); + ivCipherText.set(iv, 0); + ivCipherText.set(cipherText, iv.length); + + const macKey = await sha256(hash.slice(16)); + const hmac = await hmacSha256Sign(macKey, ivCipherText); + const ephemPublicKey = secp.getPublicKey(ephemPrivateKey, false); + + const cipher = new Uint8Array( + ephemPublicKey.length + ivCipherText.length + hmac.length + ); + let index = 0; + cipher.set(ephemPublicKey, index); + index += ephemPublicKey.length; + cipher.set(ivCipherText, index); + index += ivCipherText.length; + cipher.set(hmac, index); + return cipher; +} + +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 { + 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); + } +} diff --git a/src/lib/waku_message/index.node.spec.ts b/src/lib/waku_message/index.node.spec.ts index 5e5c953630..3d14ecf178 100644 --- a/src/lib/waku_message/index.node.spec.ts +++ b/src/lib/waku_message/index.node.spec.ts @@ -18,7 +18,7 @@ import { getPublicKey, } from "./version_1"; -import { WakuMessage } from "./index"; +import { DecryptionMethod, WakuMessage } from "./index"; const dbg = debug("waku:test:message"); @@ -66,7 +66,9 @@ describe("Waku Message [node only]", function () { const privateKey = generatePrivateKey(); - waku.relay.addDecryptionKey(privateKey); + waku.relay.addDecryptionKey(privateKey, { + method: DecryptionMethod.Asymmetric, + }); const receivedMsgPromise: Promise = new Promise( (resolve) => { @@ -132,7 +134,9 @@ describe("Waku Message [node only]", function () { dbg("Generate symmetric key"); const symKey = generateSymmetricKey(); - waku.relay.addDecryptionKey(symKey); + waku.relay.addDecryptionKey(symKey, { + method: DecryptionMethod.Symmetric, + }); const receivedMsgPromise: Promise = new Promise( (resolve) => { diff --git a/src/lib/waku_message/index.ts b/src/lib/waku_message/index.ts index c31caa461a..2a5e591b68 100644 --- a/src/lib/waku_message/index.ts +++ b/src/lib/waku_message/index.ts @@ -181,7 +181,7 @@ export class WakuMessage { return await version_1.decryptAsymmetric(payload, key); } catch (e) { dbg( - "Failed to decrypt message using symmetric encryption despite decryption method being specified", + "Failed to decrypt message using asymmetric encryption despite decryption method being specified", e ); return; diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts index 9e7339923b..4972410f16 100644 --- a/src/lib/waku_message/version_1.ts +++ b/src/lib/waku_message/version_1.ts @@ -1,12 +1,12 @@ import { Buffer } from "buffer"; import * as crypto from "crypto"; -import * as ecies from "ecies-geth"; import { keccak256 } from "js-sha3"; import * as secp256k1 from "secp256k1"; import { hexToBytes } from "../utils"; +import * as ecies from "./ecies"; import { IvSize, symmetric, SymmetricKeySize } from "./symmetric"; const FlagsLength = 1; diff --git a/src/lib/waku_relay/index.node.spec.ts b/src/lib/waku_relay/index.node.spec.ts index 38f72eff4b..f6d9b5ac47 100644 --- a/src/lib/waku_relay/index.node.spec.ts +++ b/src/lib/waku_relay/index.node.spec.ts @@ -200,6 +200,7 @@ describe("Waku Relay [node only]", () => { }); await waku1.relay.send(encryptedAsymmetricMessage); + await delay(200); await waku1.relay.send(encryptedSymmetricMessage); while (msgs.length < 2) {