From 4583515224aa78f3e0a556112e5a6eb313427c2c Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 18 Nov 2024 06:21:03 -0800 Subject: [PATCH] In BIP-32/44 keygen, add type guard against input to serialization functions and replace positional set of typed array with padding argument in call to Convert function. In Safe class, fix session storage reference. In Tools class, remove littleEndian function deprecated by bugfix. In Wallet class, substitute blakejs package for blake2b-wasm; refer to getter for Ledger service instead of private property; throw error if Ledger account ckd fails so that parent abstract ckd method can return an Account guaranteed; refactor accounts function to save an allocation; add type guard to ckd implementations; remove unused imports. In TEST_VECTORS, move session storage custom polyfill for testing to separate GLOBALS file; document source of Trezor test vectors; tweak names of constants for clarity when using. In tests, use new GLOBALS file; add ckd performance test for generating 2^15 accounts; fix BLAKE2b ckd tests by adding more public/private key checks; skip sweep tool test since it can cause test failures due to node limits; reorganize wallet generation and account derivation test suites to separate concerns; remove unknown test vectors from original library. --- package-lock.json | 31 ++++++++++++ package.json | 2 + src/lib/bip32-key-derivation.ts | 22 ++++----- src/lib/safe.ts | 12 ++--- src/lib/tools.ts | 19 ++------ src/lib/wallet.ts | 49 +++++++++---------- test/GLOBALS.mjs | 17 +++++++ test/TEST_VECTORS.js | 48 ++++++++----------- test/create-wallet.test.mjs | 84 ++++++++------------------------- test/derive-accounts.test.mjs | 23 ++++++++- test/import-wallet.test.mjs | 61 ++++++++++++++++++------ test/lock-unlock-wallet.mjs | 3 +- test/manage-rolodex.mjs | 1 + test/refresh-accounts.test.mjs | 3 +- test/sign-blocks.test.mjs | 3 +- test/tools.test.mjs | 7 ++- 16 files changed, 211 insertions(+), 174 deletions(-) create mode 100644 test/GLOBALS.mjs diff --git a/package-lock.json b/package-lock.json index 8fed5d2..1ec4b4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.19", "license": "(GPL-3.0-or-later AND MIT)", "dependencies": { + "blake2b-wasm": "^2.4.0", "blakejs": "^1.2.1" }, "devDependencies": { + "@types/blake2b-wasm": "^2.4.3", "@types/node": "^22.8.6", "@types/w3c-web-hid": "^1.0.6", "@types/w3c-web-usb": "^1.0.10", @@ -517,6 +519,13 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/@types/blake2b-wasm": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/blake2b-wasm/-/blake2b-wasm-2.4.3.tgz", + "integrity": "sha512-emsOJOuF5shxg5zhN3CHOy4BO/a26O++yk0ncFW9fePquKSGs1g6PIps8u8zFmApJjIkMQr7neVUqvoic4BRFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.8.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", @@ -548,6 +557,22 @@ "dev": true, "license": "MIT" }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "license": "MIT", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -604,6 +629,12 @@ "node": ">=0.8.x" } }, + "node_modules/nanoassert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", + "license": "ISC" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", diff --git a/package.json b/package.json index 7dee5a7..93810e4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html" }, "dependencies": { + "blake2b-wasm": "^2.4.0", "blakejs": "^1.2.1" }, "optionalDependencies": { @@ -53,6 +54,7 @@ "@ledgerhq/hw-transport-webusb": "^6.29.4" }, "devDependencies": { + "@types/blake2b-wasm": "^2.4.3", "@types/node": "^22.8.6", "@types/w3c-web-hid": "^1.0.6", "@types/w3c-web-usb": "^1.0.10", diff --git a/src/lib/bip32-key-derivation.ts b/src/lib/bip32-key-derivation.ts index 8580ad0..3bff28a 100644 --- a/src/lib/bip32-key-derivation.ts +++ b/src/lib/bip32-key-derivation.ts @@ -47,25 +47,25 @@ async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): } function ser32 (integer: number): Uint8Array { - const bits = integer.toString(2) + if (typeof integer !== 'number') { + throw new TypeError(`Expected a number, received ${typeof integer}`) + } + const bits = dec.toBin(integer) if (bits.length > 32) { - throw new RangeError(`Expected 32-bit integer, received ${bits.length} bits: ${bits}`) + throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`) } - const bytes = dec.toBytes(integer) - const result = new Uint8Array(4) - result.set(bytes, 4 - bytes.length) - return result + return dec.toBytes(integer, 4) } function ser256 (integer: string): Uint8Array { + if (typeof integer !== 'string') { + throw new TypeError(`Expected string, received ${typeof integer}`) + } const bits = hex.toBin(integer) if (bits.length > 256) { - throw new RangeError(`Expected 256-bit integer, received ${bits.length} bits: ${bits}`) + throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`) } - const bytes = hex.toBytes(integer) - const result = new Uint8Array(32) - result.set(bytes, 32 - bytes.length) - return result + return hex.toBytes(integer, 32) } async function hmac (key: Uint8Array, data: Uint8Array): Promise { diff --git a/src/lib/safe.ts b/src/lib/safe.ts index cebab59..8654756 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -4,10 +4,10 @@ import { buffer, hex, utf8 } from './convert.js' import { Entropy } from './entropy.js' const { subtle } = globalThis.crypto -const storage = globalThis.sessionStorage const ERR_MSG = 'Failed to store item in Safe' export class Safe { + #storage = globalThis.sessionStorage /** * Encrypts data with a password and stores it in the Safe. @@ -18,7 +18,7 @@ export class Safe { */ async put (name: string, key: CryptoKey, data: any): Promise async put (name: string, passkey: string | CryptoKey, data: any): Promise { - if (storage.getItem(name)) { + if (this.#storage.getItem(name)) { throw new Error(ERR_MSG) } return this.overwrite(name, passkey as string, data) @@ -60,7 +60,7 @@ export class Safe { } await new Promise((resolve, reject) => { try { - storage.setItem(name, JSON.stringify(record)) + this.#storage.setItem(name, JSON.stringify(record)) resolve() } catch (err) { reject(err) @@ -70,7 +70,7 @@ export class Safe { } catch (err) { throw new Error(ERR_MSG) } - return (storage.getItem(name) != null) + return (this.#storage.getItem(name) != null) } /** @@ -87,7 +87,7 @@ export class Safe { } const item = await new Promise(resolve => { - resolve(storage.getItem(name)) + resolve(this.#storage.getItem(name)) }) if (item == null) { return null @@ -110,7 +110,7 @@ export class Safe { const decoded = buffer.toUtf8(decrypted) const data = JSON.parse(decoded) passkey = '' - storage.removeItem(name) + this.#storage.removeItem(name) return data } catch (err) { return null diff --git a/src/lib/tools.ts b/src/lib/tools.ts index 20065ad..d5c2472 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -88,18 +88,6 @@ export async function hash (data: string | string[]): Promise { return bytes.toHex(hash) } - -/** -* Checks the endianness of the current machine. -* -* @returns {Promise} True if little-endian, else false -*/ -export async function littleEndian () { - const buffer = new ArrayBuffer(2) - new DataView(buffer).setUint16(0, 256, true) - return new Uint16Array(buffer)[0] === 256 -} - /** * Signs arbitrary strings with a private key using the Ed25519 signature scheme. * @@ -152,7 +140,7 @@ export async function sweep (rpc: Rpc | string | URL, wallet: Blake2bWallet | Bi account.representative.address, account.frontier ) - blockQueue.push(new Promise(async resolve => { + const blockRequest = new Promise(async (resolve) => { try { await block.pow(rpc) await block.sign(account.index) @@ -163,7 +151,8 @@ export async function sweep (rpc: Rpc | string | URL, wallet: Blake2bWallet | Bi } finally { resolve(null) } - })) + }) + blockQueue.push(blockRequest) } } await Promise.allSettled(blockQueue) @@ -187,4 +176,4 @@ export async function verify (key: string, signature: string, ...input: string[] hex.toBytes(signature)) } -export default { blake2b, convert, hash, littleEndian, sign, sweep, verify } +export default { blake2b, convert, hash, sign, sweep, verify } diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 3608804..b180edb 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -1,15 +1,15 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later +import blake2b from 'blake2b-wasm' import { Account } from './account.js' -import { Bip39Mnemonic } from './bip39-mnemonic.js' import { nanoCKD } from './bip32-key-derivation.js' +import { Bip39Mnemonic } from './bip39-mnemonic.js' import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js' -import { bytes, dec } from './convert.js' +import { dec, hex } from './convert.js' import { Entropy } from './entropy.js' import { Rpc } from './rpc.js' import { Safe } from './safe.js' -import Tools from './tools.js' import type { Ledger } from './ledger.js' /** @@ -42,7 +42,7 @@ abstract class Wallet { return '' } - abstract ckd (index: number): Promise + abstract ckd (index: number): Promise constructor (seed?: string, mnemonic?: Bip39Mnemonic, id?: string) { if (this.constructor === Wallet) { @@ -69,19 +69,12 @@ abstract class Wallet { from = to to = swap } - const accounts: Account[] = [] for (let i = from; i <= to; i++) { - if (this.#accounts[i]) { - accounts.push(this.#accounts[i]) - } else { - const account = await this.ckd(i) - if (account != null) { - this.#accounts[i] = account - accounts.push(account) - } + if (this.#accounts[i] == null) { + this.#accounts[i] = await this.ckd(i) } } - return accounts + return this.#accounts.slice(from, to + 1) } /** @@ -398,7 +391,10 @@ export class Bip44Wallet extends Wallet { */ async ckd (index: number): Promise { const key = await nanoCKD(this.seed, index) - return await Account.fromPrivateKey(key, index) + if (typeof key !== 'string') { + throw new TypeError('BIP-44 child key derivation returned invalid data') + } + return Account.fromPrivateKey(key, index) } } @@ -539,13 +535,12 @@ export class Blake2bWallet extends Wallet { * @returns {Promise} */ async ckd (index: number): Promise { - const indexBytes = dec.toBytes(index, 4) - if (await Tools.littleEndian()) { - indexBytes.reverse() + const input = `${this.seed}${dec.toHex(index, 8)}` + const key = blake2b().update(hex.toBytes(input)).digest('hex') + if (typeof key !== 'string') { + throw new TypeError('BLAKE2b child key derivation returned invalid data') } - const hash = await Tools.blake2b([this.seed, bytes.toHex(indexBytes)]) - const key = bytes.toHex(hash) - return await Account.fromPrivateKey(key, index) + return Account.fromPrivateKey(key, index) } } @@ -610,12 +605,12 @@ export class LedgerWallet extends Wallet { * @param {number} index - Index of the account * @returns {Promise} */ - async ckd (index: number): Promise { + async ckd (index: number): Promise { const { status, publicKey } = await this.ledger.account(index) if (status === 'OK' && publicKey != null) { return await Account.fromPublicKey(publicKey, index) } - return null + throw new Error(`Error getting Ledger account: ${status}`) } /** @@ -627,10 +622,10 @@ export class LedgerWallet extends Wallet { * @returns True if successfully locked */ async lock (): Promise { - if (this.#ledger == null) { + if (this.ledger == null) { return false } - const result = await this.#ledger.close() + const result = await this.ledger.close() return result === 'OK' } @@ -643,10 +638,10 @@ export class LedgerWallet extends Wallet { * @returns True if successfully unlocked */ async unlock (): Promise { - if (this.#ledger == null) { + if (this.ledger == null) { return false } - const result = await this.#ledger.connect() + const result = await this.ledger.connect() return result === 'OK' } } diff --git a/test/GLOBALS.mjs b/test/GLOBALS.mjs new file mode 100644 index 0000000..fa33be3 --- /dev/null +++ b/test/GLOBALS.mjs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +if (globalThis.sessionStorage == null) { + const _sessionStorage = {} + Object.defineProperty(globalThis, 'sessionStorage', { + value: { + length: Object.entries(_sessionStorage).length, + setItem: (key, value) => _sessionStorage[key] = value, + getItem: (key) => _sessionStorage[key], + removeItem: (key) => delete _sessionStorage[key], + clear: () => _sessionStorage = {} + }, + configurable: true, + enumerable: true + }) +} diff --git a/test/TEST_VECTORS.js b/test/TEST_VECTORS.js index 5af1d33..32bfdf9 100644 --- a/test/TEST_VECTORS.js +++ b/test/TEST_VECTORS.js @@ -1,23 +1,6 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -export const STORAGE = (() => { - if (globalThis.sessionStorage == null) { - const _sessionStorage = {} - Object.defineProperty(globalThis, 'sessionStorage', { - value: { - length: Object.entries(_sessionStorage).length, - setItem: (key, value) => _sessionStorage[key] = value, - getItem: (key) => _sessionStorage[key], - removeItem: (key) => delete _sessionStorage[key], - clear: () => _sessionStorage = {} - }, - configurable: true, - enumerable: true - }) - } -})() - export const GENESIS_ADDRESS = 'nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3' export const RAW_MAX = '340282366920938463463374607431768211455' export const SUPPLY_MAX = '133248297920938463463374607431768211455' @@ -40,6 +23,10 @@ export const NANO_TEST_VECTORS = Object.freeze({ ADDRESS_2: 'nano_3b5fnnerfrkt4me4wepqeqggwtfsxu8fai4n473iu6gxprfq4xd8pk9gh1dg' }) +/** +* Source: https://github.com/trezor/python-mnemonic/blob/master/vectors.json +* BLAKE2b keys calculated with Nano KeyTools: https://tools.nanos.cc/?tool=seed +*/ export const TREZOR_TEST_VECTORS = Object.freeze({ PASSWORD: 'TREZOR', @@ -47,33 +34,36 @@ export const TREZOR_TEST_VECTORS = Object.freeze({ MNEMONIC_0: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", SEED_0: "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", BIP32_KEY_0: "xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM", - NANOS_CC_PRIVATE_0: "9F0E444C69F77A49BD0BE89DB92C38FE713E0963165CCA12FAF5712D7657120F", - NANOS_CC_PUBLIC_0: "C008B814A7D269A1FA3C6528B19201A24D797912DB9996FF02A1FF356E45552B", - NANOS_CC_ADDRESS_0: "nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7", + BLAKE2B_PRIVATE_0: "9F0E444C69F77A49BD0BE89DB92C38FE713E0963165CCA12FAF5712D7657120F", + BLAKE2B_PUBLIC_0: "C008B814A7D269A1FA3C6528B19201A24D797912DB9996FF02A1FF356E45552B", + BLAKE2B_ADDRESS_0: "nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7", ENTROPY_1: "7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F", MNEMONIC_1: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", SEED_1: "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", BIP32_KEY_1: "xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU", - NANOS_CC_PRIVATE_1: "C54F9F69B088B554FF494D4CE7D23EB1B13E89D338F219F83BC91F415C3F7F2D", - NANOS_CC_PUBLIC_1: "1573BD1B96ECF80571BF544854026E6A967F065028FBC514B548471DC60B3229", - NANOS_CC_ADDRESS_1: "nano_17dmqnfsfu9r1oruyo4aci38wtnphw571c9urncdck495q51pejbp3c648yo", + BLAKE2B_1_PRIVATE_0: "C54F9F69B088B554FF494D4CE7D23EB1B13E89D338F219F83BC91F415C3F7F2D", + BLAKE2B_1_PUBLIC_0: "1573BD1B96ECF80571BF544854026E6A967F065028FBC514B548471DC60B3229", + BLAKE2B_1_ADDRESS_0: "nano_17dmqnfsfu9r1oruyo4aci38wtnphw571c9urncdck495q51pejbp3c648yo", + BLAKE2B_1_PRIVATE_1: "1B704560A0A04EAFD81E8D13481370DA458E2BB00C57F3AA00120D80F6A2BB6F", + BLAKE2B_1_PUBLIC_1: "353288BD57F98A2FC940B4D5A5CE9194EF1598611B00C629E96189320AC7409F", + BLAKE2B_1_ADDRESS_1: "nano_1fbkj4yohyec7z6n3f8onq9b579h4pe848r1rrnykreb8a7egi6z14nozo43", ENTROPY_2: "8080808080808080808080808080808080808080808080808080808080808080", MNEMONIC_2: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", SEED_2: "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", BIP32_KEY_2: "xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo", - NANOS_CC_PRIVATE_2: "554BE953D1E2DAAD0F8CBC2002967FC158E57032A6C4FD107FFEB2ACA518B613", - NANOS_CC_PUBLIC_2: "D85DECD78A303A18CC0D7B65FB384B9C49A7E2EF3666250CBD4F6EC4791513F8", - NANOS_CC_ADDRESS_2: "nano_3p4xxmdrne3t5581tyu7zew6q94bnzjgyfm86n8dtmugrjwjc6zrrci4g1rc", + BLAKE2B_2_PRIVATE_0: "554BE953D1E2DAAD0F8CBC2002967FC158E57032A6C4FD107FFEB2ACA518B613", + BLAKE2B_2_PUBLIC_0: "D85DECD78A303A18CC0D7B65FB384B9C49A7E2EF3666250CBD4F6EC4791513F8", + BLAKE2B_2_ADDRESS_0: "nano_3p4xxmdrne3t5581tyu7zew6q94bnzjgyfm86n8dtmugrjwjc6zrrci4g1rc", ENTROPY_3: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", MNEMONIC_3: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", SEED_3: "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", BIP32_KEY_3: "xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB", - NANOS_CC_PRIVATE_3: "F1FD8CBD15A54FABDED17C65C4DD44E1F93AAD122FCC1840B1EDEFAAA5BA2B22", - NANOS_CC_PUBLIC_3: "6DDE6DDEDE04254B9BC75D04017D4F4406AC7A5F7374550C1EECC8594BFB1E70", - NANOS_CC_ADDRESS_3: "nano_1ugyfqhfw337bgfwgqa617ynyj18ojx7ywuncn83xu8ad77zp9mip188iakf" + BLAKE2B_3_PRIVATE_0: "F1FD8CBD15A54FABDED17C65C4DD44E1F93AAD122FCC1840B1EDEFAAA5BA2B22", + BLAKE2B_3_PUBLIC_0: "6DDE6DDEDE04254B9BC75D04017D4F4406AC7A5F7374550C1EECC8594BFB1E70", + BLAKE2B_3_ADDRESS_0: "nano_1ugyfqhfw337bgfwgqa617ynyj18ojx7ywuncn83xu8ad77zp9mip188iakf" }) export const BIP32_TEST_VECTORS = Object.freeze({ diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs index 583e5df..4e63dc3 100644 --- a/test/create-wallet.test.mjs +++ b/test/create-wallet.test.mjs @@ -3,99 +3,53 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' -import { Account, Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' +import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere -describe('generate wallet test', async () => { - it('should fail to create a wallet when using new', () => { - assert.throws(() => new Bip44Wallet()) - assert.throws(() => new Blake2bWallet()) - assert.throws(() => new LedgerWallet()) - }) +describe('creating a new wallet', async () => { - it('should fail to create a software wallet without a password', async () => { - await assert.rejects(Bip44Wallet.create()) - await assert.rejects(Blake2bWallet.create()) - }) - - it('should replace invalid salt with empty string', async () => { - const invalidArgs = [null, true, false, 0, 1, 2, { "foo": "bar" }] - for (const arg of invalidArgs) { - await assert.doesNotReject(Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD, arg), `Rejected ${arg}`) - } - }) - - it('should generate a BIP-44 wallet with random entropy', async () => { + it('BIP-44 wallet with random entropy', async () => { const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() assert.ok('id' in wallet) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) }) - it('should generate a BLAKE2b wallet with random entropy', async () => { + it('BLAKE2b wallet with random entropy', async () => { const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() assert.ok('id' in wallet) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) }) - it('should generate the correct wallet with the given test vector', async () => { - const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C7970') - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) - assert.equal(wallet.mnemonic, 'hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') - assert.equal(wallet.seed, '1ACCDD4C25E06E47310D0C62C290EC166071D024352E003E5366E8BA6BA523F2A0CB34116AC55A238A886778880A9B2A547112FD7CFFADE81D8D8D084CCB7D36') - assert.equal(accounts[0].privateKey, 'EB18B748BCC48F824CF8A1FE92F7FC93BFC6F2A1EB9C1D40FA26D335D8A0C30F') - assert.equal(accounts[0].publicKey, 'A9EF7BBC004813CF75C5FC5C582066182D5C9CFFD42EB7EB81CEFEA8E78C47C5') - assert.equal(accounts[0].address, 'nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') + it('BIP-44 replace invalid salt with empty string', async () => { + const invalidArgs = [null, true, false, 0, 1, 2, { "foo": "bar" }] + for (const arg of invalidArgs) { + await assert.doesNotReject(Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD, arg), `Rejected ${arg}`) + } }) - it('should generate the correct wallet with the given test vector and a seed password', async () => { - const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C7970', NANO_TEST_VECTORS.PASSWORD) - await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - - assert.ok('mnemonic' in wallet) - assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) - - assert.equal(wallet.mnemonic, 'hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') - assert.notEqual(wallet.seed, '1ACCDD4C25E06E47310D0C62C290EC166071D024352E003E5366E8BA6BA523F2A0CB34116AC55A238A886778880A9B2A547112FD7CFFADE81D8D8D084CCB7D36') - assert.notEqual(accounts[0].privateKey, 'EB18B748BCC48F824CF8A1FE92F7FC93BFC6F2A1EB9C1D40FA26D335D8A0C30F') - assert.notEqual(accounts[0].publicKey, 'A9EF7BBC004813CF75C5FC5C582066182D5C9CFFD42EB7EB81CEFEA8E78C47C5') - assert.notEqual(accounts[0].address, 'nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') - - assert.equal(wallet.seed, '146E3E2A0530848C9174D45ECEC8C3F74A7BE3F1EE832F92EB6227284121EB2E48A6B8FC469403984CD5E8F0D1ED05777C78F458D0E98C911841590E5D645DC3') - assert.equal(accounts[0].privateKey, '2D5851BD5A89B8C943078BE6AD5BBEE8AEAB77D6A4744C20D1B87D78E3286B93') - assert.equal(accounts[0].publicKey, '923B6C7E281C1C5529FD2DC848117781216A1753CFD487FC34009F3591E636D7') - assert.equal(accounts[0].address, 'nano_36jufjz4i91wcnnztdgab1aqh1b3fado9mynizy5a16z8payefpqo81zsshc') + it('fail when using new', async () => { + assert.throws(() => new Bip44Wallet()) + assert.throws(() => new Blake2bWallet()) + assert.throws(() => new LedgerWallet()) }) - it('should throw when given invalid entropy with an invalid length', async () => { - assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797')) - assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701')) - assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x'))) + it('fail without a password', async () => { + await assert.rejects(Bip44Wallet.create()) + await assert.rejects(Blake2bWallet.create()) }) -}) -describe('ledger wallet', { skip: true }, async () => { - it('should connect to ledger', async () => { + it('connect to ledger', { skip: true }, async () => { const wallet = await LedgerWallet.create() assert.ok(wallet) }) diff --git a/test/derive-accounts.test.mjs b/test/derive-accounts.test.mjs index 81153ac..d35f546 100644 --- a/test/derive-accounts.test.mjs +++ b/test/derive-accounts.test.mjs @@ -3,9 +3,10 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' // WARNING: Do not send any funds to the test vectors below @@ -86,3 +87,23 @@ describe('Ledger device accounts', { skip: true }, async () => { assert.ok(accounts[0].address) }) }) + +describe('child key derivation performance', { skip: true }, async () => { + it('performance test of BIP-44 ckd', async function () { + const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + const accounts = await wallet.accounts(0, 0x7fff) + + assert.equal(accounts.length, 0x8000) + }) + + it('performance test of BLAKE2b ckd', async () => { + const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + const accounts = await wallet.accounts(0, 0x7fff) + + assert.equal(accounts.length, 0x8000) + }) +}) diff --git a/test/import-wallet.test.mjs b/test/import-wallet.test.mjs index 960c70f..30bc542 100644 --- a/test/import-wallet.test.mjs +++ b/test/import-wallet.test.mjs @@ -3,9 +3,10 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' +import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' import { Account, Bip44Wallet, Blake2bWallet } from '../dist/main.js' // WARNING: Do not send any funds to the test vectors below @@ -138,42 +139,72 @@ describe('import wallet with test vectors test', () => { }) it('should successfully import a BLAKE2b wallet with Trezor test vectors', async () => { - const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) - await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() + const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts(0, 1) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) - assert.equal(accounts[0].privateKey, TREZOR_TEST_VECTORS.NANOS_CC_PRIVATE_1) - assert.equal(accounts[0].publicKey, TREZOR_TEST_VECTORS.NANOS_CC_PUBLIC_1) - assert.equal(accounts[0].address, TREZOR_TEST_VECTORS.NANOS_CC_ADDRESS_1) + assert.ok(accounts[0] instanceof Account) + assert.equal(accounts[0].index, 0) + assert.equal(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_0) + assert.equal(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0) + assert.equal(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0) + assert.ok(accounts[1] instanceof Account) + assert.equal(accounts[1].index, 1) + assert.equal(accounts[1].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_1) + assert.equal(accounts[1].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1) + assert.equal(accounts[1].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1) }) it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => { - const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const walletAccounts = await wallet.accounts() + const walletAccount = walletAccounts[0] assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(walletAccounts[0]) - assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) + assert.ok(walletAccount) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_2) - const imported = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0) - await imported.unlock(NANO_TEST_VECTORS.PASSWORD) + const imported = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2) + await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD) const importedAccounts = await imported.accounts() + const importedAccount = importedAccounts[0] assert.equal(imported.mnemonic, wallet.mnemonic) assert.equal(imported.seed, wallet.seed) - assert.equal(importedAccounts[0].privateKey, walletAccounts[0].privateKey) - assert.equal(importedAccounts[0].publicKey, walletAccounts[0].publicKey) + assert.equal(importedAccount.privateKey, walletAccount.privateKey) + assert.equal(importedAccount.publicKey, walletAccount.publicKey) + }) + + it('should get identical BLAKE2b wallets when created with max entropy value', async () => { + const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_3) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_3) + assert.equal(accounts[0].index, 0) + assert.equal(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PRIVATE_0) + assert.equal(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0) + assert.equal(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0) }) }) describe('invalid wallet', async () => { + it('throw when given invalid entropy', async () => { + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797')) + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701')) + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x'))) + }) + it('should throw when given a seed with an invalid length', async () => { await assert.rejects(Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'), { message: `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.` }) diff --git a/test/lock-unlock-wallet.mjs b/test/lock-unlock-wallet.mjs index d8c3dab..a67089f 100644 --- a/test/lock-unlock-wallet.mjs +++ b/test/lock-unlock-wallet.mjs @@ -3,9 +3,10 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' +import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' import { Bip44Wallet, Blake2bWallet } from '../dist/main.js' const skip = false diff --git a/test/manage-rolodex.mjs b/test/manage-rolodex.mjs index bcaff1a..0d179d7 100644 --- a/test/manage-rolodex.mjs +++ b/test/manage-rolodex.mjs @@ -3,6 +3,7 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' import { Rolodex, Tools } from '../dist/main.js' diff --git a/test/refresh-accounts.test.mjs b/test/refresh-accounts.test.mjs index 3983e67..9dc71cc 100644 --- a/test/refresh-accounts.test.mjs +++ b/test/refresh-accounts.test.mjs @@ -3,9 +3,10 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' import { Account, Bip44Wallet, Rpc } from '../dist/main.js' // WARNING: Do not send any funds to the test vectors below diff --git a/test/sign-blocks.test.mjs b/test/sign-blocks.test.mjs index a02a9c2..06b1790 100644 --- a/test/sign-blocks.test.mjs +++ b/test/sign-blocks.test.mjs @@ -3,10 +3,11 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { SendBlock, ReceiveBlock, ChangeBlock } from '../dist/main.js' import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' +import { SendBlock, ReceiveBlock, ChangeBlock } from '../dist/main.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ diff --git a/test/tools.test.mjs b/test/tools.test.mjs index 9e7db51..4d98f9c 100644 --- a/test/tools.test.mjs +++ b/test/tools.test.mjs @@ -3,11 +3,14 @@ 'use strict' +import './GLOBALS.mjs' import { describe, it } from 'node:test' import { strict as assert } from 'assert' -import { RAW_MAX, NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' +import { RAW_MAX, NANO_TEST_VECTORS } from './TEST_VECTORS.js' import { Bip44Wallet, Account, SendBlock, Rpc, Tools } from '../dist/main.js' +const skip = true + const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const rpc = new Rpc(process.env.NODE_URL, process.env.API_KEY_NAME, process.env.API_KEY_VALUE) @@ -145,7 +148,7 @@ describe('sweeper', async () => { { message: 'Missing required sweep arguments' }) }) - it('fails gracefully for ineligible accounts', async () => { + it('fails gracefully for ineligible accounts', { skip }, async () => { const results = await Tools.sweep(rpc, wallet, NANO_TEST_VECTORS.ADDRESS_1) assert.ok(results) assert.equal(results.length, 1) -- 2.34.1