Remove ecies-geth (#598)

* test: specify encryption method

Makes debugging easier.

* Fix log typo

* Remove ecies-geth

Start removal of elliptic dependency and move towards exclusive usage to
CryptoSubtle.
This commit is contained in:
Franck R 2022-03-06 23:20:59 +11:00 committed by GitHub
parent ad5b3ddc7f
commit 2798376776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 245 additions and 25 deletions

View File

@ -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

19
package-lock.json generated
View File

@ -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",

View File

@ -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",

29
src/lib/crypto.ts Normal file
View File

@ -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<ArrayBuffer> {
return subtle.digest({ name: "SHA-256" }, msg);
}

View File

@ -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<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 = 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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<boolean> {
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<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 = 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<Uint8Array> {
if (encrypted.length <= metaLength) {
throw new Error(
`Invalid Ciphertext. Data is too small. It should ba at least ${metaLength} bytes`
);
} else if (encrypted[0] !== 4) {
throw new Error(
`Not a valid ciphertext. It should begin with 4 but actually begin with ${encrypted[0]}`
);
} else {
// deserialize
const ephemPublicKey = encrypted.slice(0, 65);
const cipherTextLength = encrypted.length - metaLength;
const iv = encrypted.slice(65, 65 + 16);
const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength);
const ciphertext = cipherAndIv.slice(16);
const msgMac = encrypted.slice(65 + 16 + cipherTextLength);
// check HMAC
const px = derive(privateKey, ephemPublicKey);
const hash = await kdf(px, 32);
const [encryptionKey, macKey] = await sha256(hash.slice(16)).then(
(macKey) => [hash.slice(0, 16), macKey]
);
if (!(await hmacSha256Verify(macKey, cipherAndIv, msgMac))) {
throw new Error("Incorrect MAC");
}
return aesCtrDecrypt(iv, encryptionKey, ciphertext);
}
}

View File

@ -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<WakuMessage> = 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<WakuMessage> = new Promise(
(resolve) => {

View File

@ -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;

View File

@ -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;

View File

@ -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) {