diff --git a/.changeset/gorgeous-yaks-notice.md b/.changeset/gorgeous-yaks-notice.md new file mode 100644 index 000000000..5e6369795 --- /dev/null +++ b/.changeset/gorgeous-yaks-notice.md @@ -0,0 +1,6 @@ +--- +"@ckb-lumos/crypto": minor +"@ckb-lumos/hd": minor +--- + +refactor: replace node crypto methods with @noble/hashes and @noble/ciphers diff --git a/.eslintrc.next.js b/.eslintrc.next.js index 240fa6c24..7589d0f34 100644 --- a/.eslintrc.next.js +++ b/.eslintrc.next.js @@ -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 ], diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 8fd595159..b436743f3 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -44,7 +44,8 @@ ] }, "dependencies": { - "@noble/hashes": "^1.4.0" + "@noble/hashes": "^1.4.0", + "@noble/ciphers": "^0.5.3" }, "publishConfig": { "access": "public" diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 53300c4be..cbc3681a2 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -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"; diff --git a/packages/hd-cache/tests/cache.test.ts b/packages/hd-cache/tests/cache.test.ts index 2540d8357..97e24c471 100644 --- a/packages/hd-cache/tests/cache.test.ts +++ b/packages/hd-cache/tests/cache.test.ts @@ -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, diff --git a/packages/hd/package.json b/packages/hd/package.json index 3373200c6..ff8832e8f 100644 --- a/packages/hd/package.json +++ b/packages/hd/package.json @@ -25,7 +25,6 @@ "bn.js": "^5.1.3", "elliptic": "^6.5.4", "scrypt-js": "^3.0.1", - "sha3": "^2.1.3", "uuid": "^8.3.0" }, "repository": { diff --git a/packages/hd/src/keychain.ts b/packages/hd/src/keychain.ts index 9e65f88b2..5e147034d 100644 --- a/packages/hd/src/keychain.ts +++ b/packages/hd/src/keychain.ts @@ -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"; +import { ec as EC } from "elliptic"; +import { hmac, sha256, sha512, ripemd160 } from "@ckb-lumos/crypto"; import { privateToPublic } from "./key"; const ec = new EC("secp256k1"); @@ -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; @@ -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; @@ -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; @@ -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); @@ -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("/"); @@ -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 { diff --git a/packages/hd/src/keystore.ts b/packages/hd/src/keystore.ts index 426538cba..244eaabe3 100644 --- a/packages/hd/src/keystore.ts +++ b/packages/hd/src/keystore.ts @@ -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"; @@ -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. @@ -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)); + const plaintext = Buffer.from( + extendedPrivateKey.serialize().slice(2), + "hex" + ); + const ciphertext = Buffer.from(cipher.encrypt(plaintext)); return new Keystore( { @@ -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 { @@ -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 { diff --git a/packages/hd/src/mnemonic/index.ts b/packages/hd/src/mnemonic/index.ts index b4a9f13c4..1df4e4f24 100644 --- a/packages/hd/src/mnemonic/index.ts +++ b/packages/hd/src/mnemonic/index.ts @@ -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"; @@ -39,7 +44,7 @@ function bytesToBinary(bytes: Buffer): string { 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); } @@ -50,37 +55,21 @@ function salt(password = ""): string { 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 { - 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); } export function mnemonicToEntropy(mnemonic = ""): HexString { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 671deca84..45d23e6f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -307,6 +307,9 @@ importers: packages/crypto: dependencies: + '@noble/ciphers': + specifier: ^0.5.3 + version: 0.5.3 '@noble/hashes': specifier: ^1.4.0 version: 1.4.0 @@ -474,9 +477,6 @@ importers: scrypt-js: specifier: ^3.0.1 version: 3.0.1 - sha3: - specifier: ^2.1.3 - version: 2.1.4 uuid: specifier: ^8.3.0 version: 8.3.2 @@ -4349,6 +4349,10 @@ packages: dev: true optional: true + /@noble/ciphers@0.5.3: + resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} + dev: false + /@noble/hashes@1.4.0: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -6434,13 +6438,6 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - /bytes@3.0.0: resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} engines: {node: '>= 0.8'} @@ -14985,12 +14982,6 @@ packages: safe-buffer: 5.2.1 dev: true - /sha3@2.1.4: - resolution: {integrity: sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==} - dependencies: - buffer: 6.0.3 - dev: false - /shallow-clone@3.0.1: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'}