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

refactor: replace node crypto methods with noble methods #709

Merged
merged 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/gorgeous-yaks-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ckb-lumos/crypto": minor
"@ckb-lumos/hd": minor
---

refactor: replace node crypto methods with @noble/hashes and @noble/ciphers
1 change: 1 addition & 0 deletions .eslintrc.next.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
-1, // index -1 is not found
0, // first element of an array
1, // common for i + 1 in a loop
2, // many .slice(2) since the '0x' prefix should be removed while calling 3rd-party library
16, // toString(16)
1000, // second to millisecond
],
Expand Down
3 changes: 2 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
]
},
"dependencies": {
"@noble/hashes": "^1.4.0"
"@noble/hashes": "^1.4.0",
"@noble/ciphers": "^0.5.3"
},
"publishConfig": {
"access": "public"
Expand Down
8 changes: 8 additions & 0 deletions packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export { ctr } from "@noble/ciphers/aes";
export { hmac } from "@noble/hashes/hmac";
export { sha256 } from "@noble/hashes/sha256";
export { sha512 } from "@noble/hashes/sha512";
export { ripemd160 } from "@noble/hashes/ripemd160";
export { scrypt, ScryptOpts } from "@noble/hashes/scrypt";
export { pbkdf2, pbkdf2Async } from "@noble/hashes/pbkdf2";
export { keccak_256 as keccak256 } from "@noble/hashes/sha3";
export { randomBytes } from "@noble/hashes/utils";
7 changes: 0 additions & 7 deletions packages/hd-cache/tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,6 @@ const cacheManager = CacheManager.fromMnemonic(
}
);

test.before(() => {
// @ts-ignore: Unreachable code error
BigInt = () => {
throw new Error("can not find bigint");
};
});

test("derive threshold", async (t) => {
const cacheManager = CacheManager.fromMnemonic(
indexer,
Expand Down
1 change: 0 additions & 1 deletion packages/hd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"bn.js": "^5.1.3",
"elliptic": "^6.5.4",
"scrypt-js": "^3.0.1",
homura marked this conversation as resolved.
Show resolved Hide resolved
"sha3": "^2.1.3",
"uuid": "^8.3.0"
},
"repository": {
Expand Down
31 changes: 15 additions & 16 deletions packages/hd/src/keychain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from "crypto";
import { ec as EC } from "elliptic";
/* eslint-disable @typescript-eslint/no-magic-numbers */
import BN from "bn.js";
homura marked this conversation as resolved.
Show resolved Hide resolved
import { ec as EC } from "elliptic";
import { hmac, sha256, sha512, ripemd160 } from "@ckb-lumos/crypto";
homura marked this conversation as resolved.
Show resolved Hide resolved
import { privateToPublic } from "./key";

const ec = new EC("secp256k1");
Expand All @@ -12,11 +13,11 @@ export default class Keychain {
privateKey: Buffer = EMPTY_BUFFER;
publicKey: Buffer = EMPTY_BUFFER;
chainCode: Buffer = EMPTY_BUFFER;
index: number = 0;
depth: number = 0;
index = 0;
depth = 0;
identifier: Buffer = EMPTY_BUFFER;
fingerprint: number = 0;
parentFingerprint: number = 0;
fingerprint = 0;
parentFingerprint = 0;

constructor(privateKey: Buffer, chainCode: Buffer) {
this.privateKey = privateKey;
Expand All @@ -33,10 +34,9 @@ export default class Keychain {
}

public static fromSeed(seed: Buffer): Keychain {
const i = crypto
.createHmac("sha512", Buffer.from("Bitcoin seed", "utf8"))
.update(seed)
.digest();
const i = Buffer.from(
hmac(sha512, Buffer.from("Bitcoin seed", "utf8"), seed)
);
const keychain = new Keychain(i.slice(0, 32), i.slice(32));
keychain.calculateFingerprint();
return keychain;
Expand All @@ -47,7 +47,7 @@ export default class Keychain {
public static fromPublicKey(
publicKey: Buffer,
chainCode: Buffer,
path: String
path: string
): Keychain {
const keychain = new Keychain(EMPTY_BUFFER, chainCode);
keychain.publicKey = publicKey;
Expand All @@ -74,7 +74,7 @@ export default class Keychain {
data = Buffer.concat([this.publicKey, indexBuffer]);
}

const i = crypto.createHmac("sha512", this.chainCode).update(data).digest();
const i = Buffer.from(hmac(sha512, this.chainCode, data));
const il = i.slice(0, 32);
const ir = i.slice(32);

Expand All @@ -101,7 +101,7 @@ export default class Keychain {
if (master.includes(path)) {
return this;
}

// eslint-disable-next-line @typescript-eslint/no-this-alias
let bip32: Keychain = this;

let entries = path.split("/");
Expand All @@ -117,13 +117,12 @@ export default class Keychain {
return bip32;
}

isNeutered(): Boolean {
isNeutered(): boolean {
return this.privateKey === EMPTY_BUFFER;
}

hash160(data: Buffer): Buffer {
const sha256 = crypto.createHash("sha256").update(data).digest();
return crypto.createHash("ripemd160").update(sha256).digest();
return Buffer.from(ripemd160(sha256(data)));
}

private static privateKeyAdd(privateKey: Buffer, factor: Buffer): Buffer {
Expand Down
77 changes: 34 additions & 43 deletions packages/hd/src/keystore.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import {
Cipher,
ScryptOptions,
createCipheriv,
createDecipheriv,
} from "crypto";
import { Keccak } from "sha3";
import { v4 as uuid } from "uuid";
import { ExtendedPrivateKey } from "./extended_key";
import { randomBytes } from "@ckb-lumos/crypto";
import { ctr, keccak256, randomBytes } from "@ckb-lumos/crypto";
import { HexString } from "@ckb-lumos/base";
import { syncScrypt } from "scrypt-js";

Expand All @@ -34,26 +27,33 @@ export class InvalidKeystore extends Error {
const CIPHER = "aes-128-ctr";
const CKB_CLI_ORIGIN = "ckb-cli";

interface CipherParams {
type CipherParams = {
iv: HexStringWithoutPrefix;
}
};

interface KdfParams {
type KdfParams = {
dklen: number;
n: number;
r: number;
p: number;
salt: HexStringWithoutPrefix;
}
};

interface Crypto {
type Crypto = {
cipher: string;
cipherparams: CipherParams;
ciphertext: HexStringWithoutPrefix;
kdf: string;
kdfparams: KdfParams;
mac: HexStringWithoutPrefix;
}
};

type ScryptOptions = {
N: number;
r: number;
p: number;
maxmem: number;
};

// The parameter r ("blockSize")
// specifies the block size.
Expand Down Expand Up @@ -151,19 +151,17 @@ export default class Keystore {
)
);

const cipher: Cipher = createCipheriv(CIPHER, derivedKey.slice(0, 16), iv);
if (!cipher) {
throw new UnsupportedCipher();
}

// size of 0x prefix
const hexPrefixSize = 2;
const ciphertext: Buffer = Buffer.concat([
cipher.update(
Buffer.from(extendedPrivateKey.serialize().slice(hexPrefixSize), "hex")
),
cipher.final(),
]);
// DO NOT remove the Uint8Array.from call below.
// Without calling Uint8Array.from to make a copy of iv,
// iv will be set to 0000...00000 after calling cipher.encrypt(plaintext)
// and decrypting the ciphertext will fail
/* eslint-disable @typescript-eslint/no-magic-numbers */
const cipher = ctr(derivedKey.slice(0, 16), Uint8Array.from(iv));
homura marked this conversation as resolved.
Show resolved Hide resolved
const plaintext = Buffer.from(
extendedPrivateKey.serialize().slice(2),
"hex"
);
const ciphertext = Buffer.from(cipher.encrypt(plaintext));

return new Keystore(
{
Expand Down Expand Up @@ -192,17 +190,13 @@ export default class Keystore {
if (Keystore.mac(derivedKey, ciphertext) !== this.crypto.mac) {
throw new IncorrectPassword();
}
const decipher = createDecipheriv(
this.crypto.cipher,

/* eslint-disable @typescript-eslint/no-magic-numbers */
const cipher = ctr(
derivedKey.slice(0, 16),
Buffer.from(this.crypto.cipherparams.iv, "hex")
);
return (
"0x" +
Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString(
"hex"
)
);
return "0x" + Buffer.from(cipher.decrypt(ciphertext)).toString("hex");
}

extendedPrivateKey(password: string): ExtendedPrivateKey {
Expand Down Expand Up @@ -230,15 +224,12 @@ export default class Keystore {
}

static mac(derivedKey: Buffer, ciphertext: Buffer): HexStringWithoutPrefix {
const keccakSize = 256;

return (
new Keccak(keccakSize)
// https://github.com/ethereumjs/ethereumjs-wallet/blob/d57582443fbac2b63956e6d5c4193aa8ce925b3d/src/index.ts#L615-L617
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
.update(Buffer.concat([derivedKey.subarray(16, 32), ciphertext]))
.digest("hex")
// https://github.com/ethereumjs/ethereumjs-wallet/blob/d57582443fbac2b63956e6d5c4193aa8ce925b3d/src/index.ts#L615-L617
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const hash = keccak256(
Buffer.concat([derivedKey.subarray(16, 32), ciphertext])
);
return Buffer.from(hash).toString("hex");
}

static scryptOptions(kdfparams: KdfParams): ScryptOptions {
Expand Down
49 changes: 19 additions & 30 deletions packages/hd/src/mnemonic/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import { pbkdf2, pbkdf2Sync, createHash } from "crypto";
import { randomBytes } from "@ckb-lumos/crypto";
import {
sha256,
sha512,
pbkdf2,
pbkdf2Async,
randomBytes,
} from "@ckb-lumos/crypto";
import { HexString } from "@ckb-lumos/base";
import wordList from "./word_list";

Expand Down Expand Up @@ -39,7 +44,7 @@
function deriveChecksumBits(entropyBuffer: Buffer): string {
const ENT = entropyBuffer.length * 8;
const CS = ENT / 32;
const hash = createHash("sha256").update(entropyBuffer).digest();
const hash = Buffer.from(sha256(entropyBuffer));
return bytesToBinary(hash).slice(0, CS);
}

Expand All @@ -50,37 +55,21 @@
export function mnemonicToSeedSync(mnemonic = "", password = ""): Buffer {
const mnemonicBuffer = Buffer.from(mnemonic.normalize("NFKD"), "utf8");
const saltBuffer = Buffer.from(salt(password.normalize("NFKD")), "utf8");
return pbkdf2Sync(
mnemonicBuffer,
saltBuffer,
PBKDF2_ROUNDS,
KEY_LEN,
"sha512"
return Buffer.from(
pbkdf2(sha512, mnemonicBuffer, saltBuffer, {
c: PBKDF2_ROUNDS,
dkLen: KEY_LEN,
})
);
}

export function mnemonicToSeed(mnemonic = "", password = ""): Promise<Buffer> {
return new Promise((resolve, reject) => {
try {
const mnemonicBuffer = Buffer.from(mnemonic.normalize("NFKD"), "utf8");
const saltBuffer = Buffer.from(salt(password.normalize("NFKD")), "utf8");
pbkdf2(
mnemonicBuffer,
saltBuffer,
PBKDF2_ROUNDS,
KEY_LEN,
"sha512",
(err, data) => {
if (err) {
reject(err);
}
resolve(data);
}
);
} catch (error) {
reject(error);
}
});
const mnemonicBuffer = Buffer.from(mnemonic.normalize("NFKD"), "utf8");
const saltBuffer = Buffer.from(salt(password.normalize("NFKD")), "utf8");
return pbkdf2Async(sha512, mnemonicBuffer, saltBuffer, {
c: PBKDF2_ROUNDS,
dkLen: KEY_LEN,
}).then(Buffer.from);
homura marked this conversation as resolved.
Show resolved Hide resolved
}

export function mnemonicToEntropy(mnemonic = ""): HexString {
Expand All @@ -96,7 +85,7 @@
}
const bits = words
.map((word) => {
const index = wordList!.indexOf(word);

Check warning on line 88 in packages/hd/src/mnemonic/index.ts

View workflow job for this annotation

GitHub Actions / lint-staged

Forbidden non-null assertion
if (index === -1) {
throw new Error(INVALID_MNEMONIC);
}
Expand All @@ -108,7 +97,7 @@
const entropyBits = bits.slice(0, dividerIndex);
const checksumBits = bits.slice(dividerIndex);

const entropyBytes = entropyBits

Check warning on line 100 in packages/hd/src/mnemonic/index.ts

View workflow job for this annotation

GitHub Actions / lint-staged

Forbidden non-null assertion
.match(/(.{1,8})/g)!
.map((byte) => parseInt(byte, 2));
if (entropyBytes.length < MIN_ENTROPY_SIZE) {
Expand Down Expand Up @@ -147,7 +136,7 @@
const checksumBytes = deriveChecksumBits(entropy);

const bytes = entropyBytes + checksumBytes;
const chunks = bytes.match(/(.{1,11})/g)!;

Check warning on line 139 in packages/hd/src/mnemonic/index.ts

View workflow job for this annotation

GitHub Actions / lint-staged

Forbidden non-null assertion
const words = chunks.map((binary) => {
const index = parseInt(binary, 2);
return wordList[index];
Expand Down
23 changes: 7 additions & 16 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading