Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade crypto from bcrypto to noble #197

Draft
wants to merge 33 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d533fd8
fix: filter multiaddr in nodeAddresstostring
acolytec3 May 31, 2022
ee9f1c2
Clarify comments
acolytec3 May 31, 2022
5a9c9df
lint
acolytec3 May 31, 2022
6544378
Merge remote-tracking branch 'origin/master'
acolytec3 Jul 2, 2022
9952262
partial migration to noble/libp2p-crypto
acolytec3 Jul 2, 2022
c4f7e26
switch from libp2p aes to noble hashes
acolytec3 Jul 2, 2022
69c0020
More incomplete changes
acolytec3 Jul 3, 2022
338643e
More fixes
acolytec3 Jul 4, 2022
98d16b1
switch sign from async to sync
acolytec3 Jul 4, 2022
a2e6c1e
Remove async nonsense
acolytec3 Jul 4, 2022
d5a8102
Requested fixes
acolytec3 Jul 5, 2022
4dc5620
set correct opts on secp256k1.sign
acolytec3 Jul 5, 2022
d9ff963
Fix nodeId hashing
acolytec3 Jul 5, 2022
7f70f8d
Buffer constructor clean-up
acolytec3 Jul 5, 2022
37f6d62
remove redundant async logic
acolytec3 Jul 5, 2022
7624bf8
fix v4 signature
acolytec3 Jul 5, 2022
1c925d6
Remove redundant asyncs
acolytec3 Jul 5, 2022
5cc64ac
Remove even more redundant async logic
acolytec3 Jul 5, 2022
0098fac
Make handleIncoming an arrow function again
acolytec3 Jul 5, 2022
19b0721
Fix decodeHeader
acolytec3 Jul 5, 2022
9c9fee6
Remove redundant async logic
acolytec3 Jul 5, 2022
855277b
restore arrow function
acolytec3 Jul 5, 2022
bb2e797
Remove bcrypto and dgram deps
acolytec3 Jul 5, 2022
89e1a45
Remove redundant Uint8Array conversions
acolytec3 Jul 5, 2022
5d24880
Use buffer.from view
acolytec3 Jul 5, 2022
aaec0e8
lint fixes
acolytec3 Jul 5, 2022
384adb5
add lint:fix script :-)
acolytec3 Jul 5, 2022
af5c232
Make linter happy
acolytec3 Jul 5, 2022
f4572f4
consolidate secp in crypto utility
acolytec3 Jul 5, 2022
66c1753
Use crypto instead of noble re-export
acolytec3 Jul 5, 2022
ad5c5df
Fix aes cipher mode
acolytec3 Jul 7, 2022
1035bfd
add benchmark script
acolytec3 Jul 7, 2022
612a76d
Add more complete crypto benchmark suite
acolytec3 Jul 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions bench/crypto/index.bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { itBench } from "@dapplion/benchmark";
import { expect } from "chai";
import { randomBytes } from "crypto";
import {
decryptMessage,
encryptMessage,
createKeypair,
KeypairType,
idSign,
idVerify,
v4,
Secp256k1Keypair,
deriveKey,
generateIdSignatureInput,
} from "../../lib/index.js";
import { hash } from "../../lib/enr/v4.js";

itBench("benchmark aes cipher encryption/decryptions", () => {
const key = Buffer.from(randomBytes(16));
const nonce = Buffer.from(randomBytes(12));
const msg = Buffer.from(randomBytes(16));
const ad = Buffer.from(randomBytes(16));

const cipher = encryptMessage(key, nonce, msg, ad);
const decrypted = decryptMessage(key, nonce, cipher, ad);
expect(decrypted).to.deep.equal(msg);
});

itBench("benchmark sign/verify", () => {
const expected = Buffer.from(
"94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6",
"hex"
);

const localSK = Buffer.from("fb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736", "hex");
const challengeData = Buffer.from(
"000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000",
"hex"
);
const ephemPK = Buffer.from("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231", "hex");
const nodeIdB = "bbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9";
const keypair = createKeypair(KeypairType.Secp256k1, localSK);
const actual = idSign(keypair, challengeData, ephemPK, nodeIdB);
expect(actual).to.deep.equal(expected);
expect(
idVerify(
createKeypair(KeypairType.Secp256k1, undefined, v4.publicKey(localSK)),
challengeData,
ephemPK,
nodeIdB,
actual
)
).to.be.true;
});

itBench("benchmark key derivation", () => {
const secret = Buffer.from("022c82f214eb37159111712add00040fcdf73fd4d7d0b7c0f980da4d099aa59ba4");
const firstNodeId = "aaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb";
const secondNodeId = "bbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9";
const challengeData = Buffer.from(
"000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000",
"hex"
);

deriveKey(Buffer.from(secret), firstNodeId, secondNodeId, challengeData);
});

itBench("benchmark secp256k1.generatePrivateKey", () => {
Secp256k1Keypair.generate();
});

itBench("benchmark keccak hash", () => {
hash(Buffer.from("secret message"));
});

itBench("benchmark sha256 hash", () => {
const challengeData = Buffer.from(
"000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000",
"hex"
);
const ephemPK = Buffer.from("039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231", "hex");
const nodeId = "bbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9";
generateIdSignatureInput(challengeData, ephemPK, nodeId);
})
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"build": "tsc -p tsconfig.build.json",
"prepublishOnly": "yarn build",
"lint": "eslint --color --ext .ts src/ test/",
"lint:fix": "eslint --color --fix --ext .ts src/ test/",
"test": "yarn test:unit && yarn test:e2e",
"test:unit": "mocha 'test/unit/**/*.test.ts'",
"test:e2e": "mocha 'test/e2e/**/*.test.ts'"
Expand Down Expand Up @@ -99,11 +100,11 @@
"@libp2p/interfaces": "^3.0.2",
"@libp2p/peer-id": "^1.1.13",
"@multiformats/multiaddr": "^10.2.0",
"@noble/hashes": "^1.1.2",
"@noble/secp256k1": "^1.6.0",
"base64url": "^3.0.1",
"bcrypto": "^5.4.0",
"bigint-buffer": "^1.1.5",
"debug": "^4.3.1",
"dgram": "^1.0.1",
"err-code": "^3.0.1",
"ip6addr": "^0.2.3",
"is-ip": "^3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/enr/enr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,6 @@ export class ENR extends Map<ENRKey, ENRValue> {
return encoded;
}
encodeTxt(privateKey?: Buffer): string {
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
return "enr:" + base64url.encode(Buffer.from(this.encode(privateKey)));
return "enr:" + base64url.encode(this.encode(privateKey));
}
}
23 changes: 12 additions & 11 deletions src/enr/v4.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import keccak from "bcrypto/lib/keccak.js";
import secp256k1 from "bcrypto/lib/secp256k1.js";
import { keccak_256 as keccak } from "@noble/hashes/sha3";
import { secp256k1 } from "../util/crypto.js";

import { NodeId } from "./types.js";
import { createNodeId } from "./create.js";
import { secp256k1PublicKeyToRaw } from "../keypair/secp256k1.js";

export function hash(input: Buffer): Buffer {
return keccak.digest(input);
return Buffer.from(keccak(input).buffer);
}

export function createPrivateKey(): Buffer {
return secp256k1.privateKeyGenerate();
return Buffer.from(secp256k1.utils.randomPrivateKey().buffer);
}

export function publicKey(privKey: Buffer): Buffer {
return secp256k1.publicKeyCreate(privKey);
return Buffer.from(secp256k1.getPublicKey(privKey, true).buffer);
}

export function sign(privKey: Buffer, msg: Buffer): Buffer {
return secp256k1.sign(hash(msg), privKey);
return Buffer.from(secp256k1.signSync(hash(msg), privKey, { der: false }).buffer);
}

export function verify(pubKey: Buffer, msg: Buffer, sig: Buffer): boolean {
return secp256k1.verify(hash(msg), sig, pubKey);
return secp256k1.verify(sig, hash(msg), pubKey);
}

export function nodeId(pubKey: Buffer): NodeId {
return createNodeId(hash(secp256k1.publicKeyConvert(pubKey, false).slice(1)));
return createNodeId(hash(secp256k1PublicKeyToRaw(pubKey)));
}

export class ENRKeyPair {
Expand All @@ -35,7 +36,7 @@ export class ENRKeyPair {

public constructor(privateKey?: Buffer) {
if (privateKey) {
if (!secp256k1.privateKeyVerify(privateKey)) {
if (!secp256k1.utils.isValidPrivateKey(privateKey)) {
throw new Error("Invalid private key");
}
}
Expand All @@ -44,8 +45,8 @@ export class ENRKeyPair {
this.nodeId = nodeId(this.publicKey);
}

public sign(msg: Buffer): Buffer {
return sign(this.privateKey, msg);
public async sign(msg: Buffer): Promise<Buffer> {
return await sign(this.privateKey, msg);
}

public verify(msg: Buffer, sig: Buffer): boolean {
Expand Down
34 changes: 19 additions & 15 deletions src/keypair/secp256k1.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import secp256k1 from "bcrypto/lib/secp256k1.js";
import { AbstractKeypair, IKeypair, IKeypairClass, KeypairType } from "./types.js";
import { ERR_INVALID_KEYPAIR_TYPE } from "./constants.js";
import { secp256k1 } from "../util/crypto.js";

export function secp256k1PublicKeyToCompressed(publicKey: Buffer): Buffer {
if (publicKey.length === 64) {
publicKey = Buffer.concat([Buffer.from([4]), publicKey]);
}
return secp256k1.publicKeyConvert(publicKey, true);
return Buffer.from(secp256k1.Point.fromHex(publicKey).toRawBytes(true));
}

export function secp256k1PublicKeyToFull(publicKey: Buffer): Buffer {
if (publicKey.length === 64) {
return Buffer.concat([Buffer.from([4]), publicKey]);
}
return secp256k1.publicKeyConvert(publicKey, false);
return Buffer.from(secp256k1.Point.fromHex(publicKey).toRawBytes(false).buffer);
}

export function secp256k1PublicKeyToRaw(publicKey: Buffer): Buffer {
return secp256k1.publicKeyConvert(publicKey, false).slice(1);
return Buffer.from(secp256k1.Point.fromHex(publicKey).toRawBytes(false).buffer).slice(1);
}

export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair extends AbstractKeypair implements IKeypair {
Expand All @@ -33,33 +33,37 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair extends Ab
}

static generate(): Secp256k1Keypair {
const privateKey = secp256k1.privateKeyGenerate();
const publicKey = secp256k1.publicKeyCreate(privateKey);
return new Secp256k1Keypair(privateKey, publicKey);
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey);
return new Secp256k1Keypair(Buffer.from(privateKey.buffer), Buffer.from(publicKey.buffer));
}

privateKeyVerify(key = this._privateKey): boolean {
if (key) {
return secp256k1.privateKeyVerify(key);
return secp256k1.utils.isValidPrivateKey(key);
}
return true;
}
publicKeyVerify(key = this._publicKey): boolean {
if (key) {
return secp256k1.publicKeyVerify(key);

publicKeyVerify(): boolean {
try {
secp256k1.Point.fromHex(this.publicKey).assertValidity();
return true;
} catch {
return false;
}
return true;
}
sign(msg: Buffer): Buffer {
return secp256k1.sign(msg, this.privateKey);
return Buffer.from(secp256k1.signSync(msg, this.privateKey, { der: false }).buffer);
}
verify(msg: Buffer, sig: Buffer): boolean {
return secp256k1.verify(msg, sig, this.publicKey);
return secp256k1.verify(sig, msg, this.publicKey);
}
deriveSecret(keypair: IKeypair): Buffer {
if (keypair.type !== this.type) {
throw new Error(ERR_INVALID_KEYPAIR_TYPE);
}
return secp256k1.derive(keypair.publicKey, this.privateKey);
const secret = Buffer.from(secp256k1.getSharedSecret(this.privateKey, keypair.publicKey, true).buffer);
return secret;
}
};
4 changes: 2 additions & 2 deletions src/message/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { randomBytes } from "bcrypto/lib/random.js";
import { randomBytes } from "@noble/hashes/utils";
import { toBigIntBE } from "bigint-buffer";

import {
Expand All @@ -14,7 +14,7 @@ import {
import { SequenceNumber, ENR } from "../enr/index.js";

export function createRequestId(): RequestId {
return toBigIntBE(randomBytes(8));
return toBigIntBE(Buffer.from(randomBytes(8).buffer));
}

export function createPingMessage(enrSeq: SequenceNumber): IPingMessage {
Expand Down
16 changes: 10 additions & 6 deletions src/packet/create.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { randomBytes } from "bcrypto/lib/random.js";
import { randomBytes } from "@noble/hashes/utils";
import { NodeId, SequenceNumber } from "../enr/index.js";
import { ID_NONCE_SIZE, MASKING_IV_SIZE, NONCE_SIZE } from "./constants.js";
import { encodeMessageAuthdata, encodeWhoAreYouAuthdata } from "./encode.js";
import { IHeader, IPacket, PacketType } from "./types.js";

export function createHeader(flag: PacketType, authdata: Buffer, nonce = randomBytes(NONCE_SIZE)): IHeader {
export function createHeader(
flag: PacketType,
authdata: Buffer,
nonce = Buffer.from(randomBytes(NONCE_SIZE))
): IHeader {
return {
protocolId: "discv5",
version: 1,
Expand All @@ -18,8 +22,8 @@ export function createHeader(flag: PacketType, authdata: Buffer, nonce = randomB
export function createRandomPacket(srcId: NodeId): IPacket {
const authdata = encodeMessageAuthdata({ srcId });
const header = createHeader(PacketType.Message, authdata);
const maskingIv = randomBytes(MASKING_IV_SIZE);
const message = randomBytes(44);
const maskingIv = Buffer.from(randomBytes(MASKING_IV_SIZE).buffer);
const message = Buffer.from(randomBytes(44).buffer);
return {
maskingIv,
header,
Expand All @@ -28,10 +32,10 @@ export function createRandomPacket(srcId: NodeId): IPacket {
}

export function createWhoAreYouPacket(nonce: Buffer, enrSeq: SequenceNumber): IPacket {
const idNonce = randomBytes(ID_NONCE_SIZE);
const idNonce = Buffer.from(randomBytes(ID_NONCE_SIZE).buffer);
const authdata = encodeWhoAreYouAuthdata({ idNonce, enrSeq });
const header = createHeader(PacketType.WhoAreYou, authdata, nonce);
const maskingIv = randomBytes(MASKING_IV_SIZE);
const maskingIv = Buffer.from(randomBytes(MASKING_IV_SIZE).buffer);
const message = Buffer.alloc(0);
return {
maskingIv,
Expand Down
9 changes: 4 additions & 5 deletions src/packet/encode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cipher from "bcrypto/lib/cipher.js";
import Crypto from "crypto";
import { toBigIntBE, toBufferBE } from "bigint-buffer";
import errcode from "err-code";

Expand Down Expand Up @@ -34,8 +34,7 @@ export function encodePacket(destId: string, packet: IPacket): Buffer {
}

export function encodeHeader(destId: string, maskingIv: Buffer, header: IHeader): Buffer {
const ctx = new cipher.Cipher("AES-128-CTR");
ctx.init(fromHex(destId).slice(0, MASKING_KEY_SIZE), maskingIv);
const ctx = Crypto.createCipheriv("aes-128-ctr", fromHex(destId).slice(0, MASKING_KEY_SIZE), maskingIv);
return ctx.update(
Buffer.concat([
// static header
Expand Down Expand Up @@ -74,8 +73,8 @@ export function decodePacket(srcId: string, data: Buffer): IPacket {
* Return the decoded header and the header as a buffer
*/
export function decodeHeader(srcId: string, maskingIv: Buffer, data: Buffer): [IHeader, Buffer] {
const ctx = new cipher.Decipher("AES-128-CTR");
ctx.init(fromHex(srcId).slice(0, MASKING_KEY_SIZE), maskingIv);
const ctx = Crypto.createDecipheriv("aes-128-ctr", fromHex(srcId).slice(0, MASKING_KEY_SIZE), maskingIv);

// unmask the static header
const staticHeaderBuf = ctx.update(data.slice(0, STATIC_HEADER_SIZE));

Expand Down
21 changes: 11 additions & 10 deletions src/session/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import hkdf from "bcrypto/lib/hkdf.js";
import sha256 from "bcrypto/lib/sha256.js";
import cipher from "bcrypto/lib/cipher.js";

import * as hkdf from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import Crypto from "crypto";
import { NodeId } from "../enr/index.js";
import { generateKeypair, IKeypair, createKeypair } from "../keypair/index.js";
import { fromHex } from "../util/index.js";
Expand Down Expand Up @@ -46,7 +45,9 @@ export function generateSessionKeys(

export function deriveKey(secret: Buffer, firstId: NodeId, secondId: NodeId, challengeData: Buffer): [Buffer, Buffer] {
const info = Buffer.concat([Buffer.from(KEY_AGREEMENT_STRING), fromHex(firstId), fromHex(secondId)]);
const output = hkdf.expand(sha256, hkdf.extract(sha256, secret, challengeData), info, 2 * KEY_LENGTH);
const output = Buffer.from(
hkdf.expand(sha256, hkdf.extract(sha256, secret, challengeData), info, 2 * KEY_LENGTH).buffer
);
return [output.slice(0, KEY_LENGTH), output.slice(KEY_LENGTH, 2 * KEY_LENGTH)];
}

Expand Down Expand Up @@ -80,15 +81,16 @@ export function idVerify(
}

export function generateIdSignatureInput(challengeData: Buffer, ephemPK: Buffer, nodeId: NodeId): Buffer {
return sha256.digest(Buffer.concat([Buffer.from(ID_SIGNATURE_TEXT), challengeData, ephemPK, fromHex(nodeId)]));
const hash = sha256(Buffer.concat([Buffer.from(ID_SIGNATURE_TEXT), challengeData, ephemPK, fromHex(nodeId)]));
return Buffer.from(hash.buffer);
}

export function decryptMessage(key: Buffer, nonce: Buffer, data: Buffer, aad: Buffer): Buffer {
if (data.length < MAC_LENGTH) {
throw new Error("message data not long enough");
}
const ctx = new cipher.Decipher("AES-128-GCM");
ctx.init(key, nonce);

const ctx = Crypto.createDecipheriv("aes-128-gcm", key, nonce);
ctx.setAAD(aad);
ctx.setAuthTag(data.slice(data.length - MAC_LENGTH));
return Buffer.concat([
Expand All @@ -98,8 +100,7 @@ export function decryptMessage(key: Buffer, nonce: Buffer, data: Buffer, aad: Bu
}

export function encryptMessage(key: Buffer, nonce: Buffer, data: Buffer, aad: Buffer): Buffer {
const ctx = new cipher.Cipher("AES-128-GCM");
ctx.init(key, nonce);
const ctx = Crypto.createCipheriv("aes-128-gcm", key, nonce);
ctx.setAAD(aad);
return Buffer.concat([
ctx.update(data),
Expand Down
1 change: 1 addition & 0 deletions src/session/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte
const entry = this.pendingRequests.get(nodeAddrStr);
if (entry) {
// if it exists, there must be a request here
//eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const request = entry.shift()!;
if (!entry.length) {
this.pendingRequests.delete(nodeAddrStr);
Expand Down
Loading