Skip to content

Commit

Permalink
Merge pull request #709 from ckb-js/crypto-to-noble
Browse files Browse the repository at this point in the history
refactor: replace node crypto methods with noble methods
  • Loading branch information
twhy authored Jun 7, 2024
2 parents d4c2cba + d17df9e commit d13f880
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 114 deletions.
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",
"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";
import { ec as EC } from "elliptic";
import { hmac, sha256, sha512, ripemd160 } from "@ckb-lumos/crypto";
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));
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 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);
}

Expand All @@ -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<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);
}

export function mnemonicToEntropy(mnemonic = ""): HexString {
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.

0 comments on commit d13f880

Please sign in to comment.