From da7b35f4765f27e4342858ea044b7ce4749f7249 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 2 Jul 2025 21:08:28 -0700 Subject: [PATCH] Extract wallets into their own files. --- src/lib/wallets/bip44-wallet.ts | 209 +++++++++++++ src/lib/wallets/blake2b-wallet.ts | 157 ++++++++++ src/lib/wallets/index.ts | 470 +----------------------------- src/lib/wallets/ledger-wallet.ts | 114 ++++++++ 4 files changed, 487 insertions(+), 463 deletions(-) create mode 100644 src/lib/wallets/bip44-wallet.ts create mode 100644 src/lib/wallets/blake2b-wallet.ts create mode 100644 src/lib/wallets/ledger-wallet.ts diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts new file mode 100644 index 0000000..a34b617 --- /dev/null +++ b/src/lib/wallets/bip44-wallet.ts @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { KeyPair, Wallet } from '.' +import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' +import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js' +import { Entropy } from '#src/lib/entropy.js' +import { Pool } from '#src/lib/pool.js' +import { Bip44CkdWorker } from '#workers' + +/** +* Hierarchical deterministic (HD) wallet created by using a source of entropy to +* derive a mnemonic phrase. The mnemonic phrase, in combination with an optional +* salt, is used to generate a seed. A value can be provided as a parameter for +* entropy, mnemonic + salt, or seed; if no argument is passed, a new entropy +* value will be generated using a cryptographically strong pseudorandom number +* generator. +* +* Importantly, the salt is not stored in the instantiated Wallet object. If a +* salt is used, then losing it means losing the ability to regenerate the seed +* from the mnemonic. +* +* Accounts are derived from the seed. Private keys are derived using a BIP-44 +* derivation path. The public key is derived from the private key using the +* Ed25519 key algorithm. Account addresses are derived as described in the nano +* documentation (https://docs.nano.org) +* +* A password must be provided when creating or importing the wallet and is used +* to lock and unlock the wallet. The wallet will be initialized as locked. When +* the wallet is unlocked, a new password can be specified using the lock() +* method. +*/ +export class Bip44Wallet extends Wallet { + static #isInternal: boolean = false + #poolBip44Ckd: Pool + + constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) { + if (!Bip44Wallet.#isInternal) { + throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) + } + Bip44Wallet.#isInternal = false + super(id, seed, mnemonic) + this.#poolBip44Ckd = new Pool(Bip44CkdWorker) + } + + /** + * Removes encrypted secrets in storage and releases variable references to + * allow garbage collection. + */ + destroy () { + super.destroy() + this.#poolBip44Ckd.terminate() + } + + /** + * Creates a new HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async create (password: string, salt?: string): Promise + /** + * Creates a new HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async create (key: CryptoKey, salt?: string): Promise + static async create (passkey: string | CryptoKey, salt: string = ''): Promise { + try { + const e = await Entropy.create() + return await Bip44Wallet.fromEntropy(passkey as string, e.hex, salt) + } catch (err) { + throw new Error(`Error creating new Bip44Wallet: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated entropy value. The user + * must ensure that it is cryptographically strongly random. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} entropy - Used when generating the initial mnemonic phrase + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromEntropy (password: string, entropy: string, salt?: string): Promise + /** + * Creates a new HD wallet by using a pregenerated entropy value. The user + * must ensure that it is cryptographically strongly random. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} entropy - Used when generating the initial mnemonic phrase + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromEntropy (key: CryptoKey, entropy: string, salt?: string): Promise + static async fromEntropy (passkey: string | CryptoKey, entropy: string, salt: string = ''): Promise { + try { + const id = await Entropy.create(16) + const e = await Entropy.import(entropy) + const m = await Bip39Mnemonic.fromEntropy(e.hex) + const s = await m.toBip39Seed(salt) + Bip44Wallet.#isInternal = true + const wallet = new this(id, s, m) + await wallet.lock(passkey as string) + return wallet + } catch (err) { + throw new Error(`Error importing Bip44Wallet from entropy: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated mnemonic phrase. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromMnemonic (password: string, mnemonic: string, salt?: string): Promise + /** + * Creates a new HD wallet by using a pregenerated mnemonic phrase. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromMnemonic (key: CryptoKey, mnemonic: string, salt?: string): Promise + static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string, salt: string = ''): Promise { + try { + const id = await Entropy.create(16) + const m = await Bip39Mnemonic.fromPhrase(mnemonic) + const s = await m.toBip39Seed(salt) + Bip44Wallet.#isInternal = true + const wallet = new this(id, s, m) + await wallet.lock(passkey as string) + return wallet + } catch (err) { + throw new Error(`Error importing Bip44Wallet from mnemonic: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated seed value. This seed cannot + * be used to regenerate any higher level randomness which includes entropy, + * mnemonic phrase, and salt. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromSeed (password: string, seed: string): Promise + /** + * Creates a new HD wallet by using a pregenerated seed value. This seed cannot + * be used to regenerate any higher level randomness which includes entropy, + * mnemonic phrase, and salt. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromSeed (key: CryptoKey, seed: string): Promise + static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { + if (seed.length !== SEED_LENGTH_BIP44) { + throw new Error(`Expected a ${SEED_LENGTH_BIP44}-character seed, but received ${seed.length}-character string.`) + } + if (!/^[0-9a-fA-F]+$/i.test(seed)) { + throw new Error('Seed contains invalid hexadecimal characters.') + } + const id = await Entropy.create(16) + Bip44Wallet.#isInternal = true + const wallet = new this(id, seed) + await wallet.lock(passkey as string) + return wallet + } + + /** + * Retrieves an existing HD wallet from session storage using its ID. + * + * @param {string} id - Generated when the wallet was initially created + * @returns {Bip44Wallet} Restored locked Bip44Wallet + */ + static async restore (id: string): Promise { + if (typeof id !== 'string' || id === '') { + throw new TypeError('Wallet ID is required to restore') + } + Bip44Wallet.#isInternal = true + return new this(await Entropy.import(id), '') + } + + /** + * Derives BIP-44 Nano account private keys. + * + * @param {number[]} indexes - Indexes of the accounts + * @returns {Promise} + */ + async ckd (indexes: number[]): Promise { + const data: any = [] + indexes.forEach(i => data.push({ seed: this.seed, index: i })) + const privateKeys: KeyPair[] = await this.#poolBip44Ckd.assign(data) + return privateKeys + } +} diff --git a/src/lib/wallets/blake2b-wallet.ts b/src/lib/wallets/blake2b-wallet.ts new file mode 100644 index 0000000..8bbb231 --- /dev/null +++ b/src/lib/wallets/blake2b-wallet.ts @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { KeyPair, Wallet } from '.' +import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' +import { Blake2b } from '#src/lib/blake2b.js' +import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js' +import { hex } from '#src/lib/convert.js' +import { Entropy } from '#src/lib/entropy.js' + +/** +* BLAKE2b wallet created by deriving a mnemonic phrase from a seed or vice +* versa. If no value is provided for either, a new BIP-39 seed and mnemonic will +* be generated using a cryptographically strong pseudorandom number generator. +* +* Account private keys are derived on an ad hoc basis using the Blake2b hashing +* function. Account public key are derived from the private key using the +* Ed25519 key algorithm. Account addresses are derived from the public key as +* described in the Nano documentation. +* https://docs.nano.org/integration-guides/the-basics/ +* +* A password must be provided when creating or importing the wallet and is used +* to lock and unlock the wallet. The wallet will be initialized as locked. When +* the wallet is unlocked, a new password can be specified using the lock() +* method. +*/ +export class Blake2bWallet extends Wallet { + static #isInternal: boolean = false + + constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) { + if (!Blake2bWallet.#isInternal) { + throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) + } + Blake2bWallet.#isInternal = false + super(id, seed, mnemonic) + } + + /** + * Creates a new BLAKE2b wallet by using a seed generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async create (password: string): Promise + /** + * Creates a new BLAKE2b wallet by using a seed generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async create (key: CryptoKey): Promise + static async create (passkey: string | CryptoKey): Promise { + try { + const seed = await Entropy.create() + return await Blake2bWallet.fromSeed(passkey as string, seed.hex) + } catch (err) { + throw new Error(`Error creating new Blake2bWallet: ${err}`) + } + } + + /** + * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must + * ensure that it is cryptographically strongly random. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromSeed (password: string, seed: string): Promise + /** + * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must + * ensure that it is cryptographically strongly random. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromSeed (key: CryptoKey, seed: string): Promise + static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { + if (seed.length !== SEED_LENGTH_BLAKE2B) { + throw new Error(`Expected a ${SEED_LENGTH_BLAKE2B}-character seed, but received ${seed.length}-character string.`) + } + if (!/^[0-9a-fA-F]+$/i.test(seed)) { + throw new Error('Seed contains invalid hexadecimal characters.') + } + const id = await Entropy.create(16) + const s = seed + const m = await Bip39Mnemonic.fromEntropy(seed) + Blake2bWallet.#isInternal = true + const wallet = new this(id, s, m) + await wallet.lock(passkey as string) + return wallet + } + + /** + * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromMnemonic (password: string, mnemonic: string): Promise + /** + * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromMnemonic (key: CryptoKey, mnemonic: string): Promise + static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string): Promise { + try { + const id = await Entropy.create(16) + const m = await Bip39Mnemonic.fromPhrase(mnemonic) + const s = await m.toBlake2bSeed() + Blake2bWallet.#isInternal = true + const wallet = new this(id, s, m) + await wallet.lock(passkey as string) + return wallet + } catch (err) { + throw new Error(`Error importing Blake2bWallet from mnemonic: ${err}`) + } + } + + /** + * Retrieves an existing BLAKE2b wallet from session storage using its ID. + * + * @param {string} id - Generated when the wallet was initially created + * @returns {Blake2bWallet} Restored locked Blake2bWallet + */ + static async restore (id: string): Promise { + if (typeof id !== 'string' || id === '') { + throw new TypeError('Wallet ID is required to restore') + } + Blake2bWallet.#isInternal = true + return new this(await Entropy.import(id), '') + } + + /** + * Derives BLAKE2b account private keys. + * + * @param {number[]} indexes - Indexes of the accounts + * @returns {Promise} + */ + async ckd (indexes: number[]): Promise { + const results = indexes.map(index => { + const indexHex = index.toString(16).padStart(8, '0').toUpperCase() + const inputHex = `${this.seed}${indexHex}`.padStart(72, '0') + const inputBytes = hex.toBytes(inputHex) + const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex') + return { privateKey, index } + }) + return results + } +} diff --git a/src/lib/wallets/index.ts b/src/lib/wallets/index.ts index 79dfc57..5d167d6 100644 --- a/src/lib/wallets/index.ts +++ b/src/lib/wallets/index.ts @@ -3,17 +3,17 @@ import { Account, AccountList } from '#src/lib/account.js' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' -import { Blake2b } from '#src/lib/blake2b.js' -import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js' -import { hex } from '#src/lib/convert.js' +import { ADDRESS_GAP } from '#src/lib/constants.js' import { Entropy } from '#src/lib/entropy.js' -import { Ledger } from '#src/lib/ledger.js' import { Pool } from '#src/lib/pool.js' import { Rpc } from '#src/lib/rpc.js' import { Safe } from '#src/lib/safe.js' -import { Bip44CkdWorker, NanoNaClWorker } from '#workers' +import { NanoNaClWorker } from '#workers' -type KeyPair = { +export { Bip44Wallet } from './bip44-wallet' +export { Blake2bWallet } from './blake2b-wallet' +export { LedgerWallet } from './ledger-wallet' +export type KeyPair = { publicKey?: string, privateKey?: string, index?: number @@ -26,7 +26,7 @@ type KeyPair = { * types of wallets are supported, each as a derived class: Bip44Wallet, * Blake2bWallet, LedgerWallet. */ -abstract class Wallet { +export abstract class Wallet { #accounts: AccountList #id: Entropy #locked: boolean = true @@ -302,459 +302,3 @@ abstract class Wallet { return true } } - -/** -* Hierarchical deterministic (HD) wallet created by using a source of entropy to -* derive a mnemonic phrase. The mnemonic phrase, in combination with an optional -* salt, is used to generate a seed. A value can be provided as a parameter for -* entropy, mnemonic + salt, or seed; if no argument is passed, a new entropy -* value will be generated using a cryptographically strong pseudorandom number -* generator. -* -* Importantly, the salt is not stored in the instantiated Wallet object. If a -* salt is used, then losing it means losing the ability to regenerate the seed -* from the mnemonic. -* -* Accounts are derived from the seed. Private keys are derived using a BIP-44 -* derivation path. The public key is derived from the private key using the -* Ed25519 key algorithm. Account addresses are derived as described in the nano -* documentation (https://docs.nano.org) -* -* A password must be provided when creating or importing the wallet and is used -* to lock and unlock the wallet. The wallet will be initialized as locked. When -* the wallet is unlocked, a new password can be specified using the lock() -* method. -*/ -export class Bip44Wallet extends Wallet { - static #isInternal: boolean = false - #poolBip44Ckd: Pool - - constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) { - if (!Bip44Wallet.#isInternal) { - throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) - } - Bip44Wallet.#isInternal = false - super(id, seed, mnemonic) - this.#poolBip44Ckd = new Pool(Bip44CkdWorker) - } - - /** - * Removes encrypted secrets in storage and releases variable references to - * allow garbage collection. - */ - destroy () { - super.destroy() - this.#poolBip44Ckd.terminate() - } - - /** - * Creates a new HD wallet by using an entropy value generated using a - * cryptographically strong pseudorandom number generator. - * - * @param {string} password - Encrypts the wallet to lock and unlock it - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async create (password: string, salt?: string): Promise - /** - * Creates a new HD wallet by using an entropy value generated using a - * cryptographically strong pseudorandom number generator. - * - * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async create (key: CryptoKey, salt?: string): Promise - static async create (passkey: string | CryptoKey, salt: string = ''): Promise { - try { - const e = await Entropy.create() - return await Bip44Wallet.fromEntropy(passkey as string, e.hex, salt) - } catch (err) { - throw new Error(`Error creating new Bip44Wallet: ${err}`) - } - } - - /** - * Creates a new HD wallet by using a pregenerated entropy value. The user - * must ensure that it is cryptographically strongly random. - * - * @param {string} password - Used to lock and unlock the wallet - * @param {string} entropy - Used when generating the initial mnemonic phrase - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromEntropy (password: string, entropy: string, salt?: string): Promise - /** - * Creates a new HD wallet by using a pregenerated entropy value. The user - * must ensure that it is cryptographically strongly random. - * - * @param {CryptoKey} key - Used to lock and unlock the wallet - * @param {string} entropy - Used when generating the initial mnemonic phrase - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromEntropy (key: CryptoKey, entropy: string, salt?: string): Promise - static async fromEntropy (passkey: string | CryptoKey, entropy: string, salt: string = ''): Promise { - try { - const id = await Entropy.create(16) - const e = await Entropy.import(entropy) - const m = await Bip39Mnemonic.fromEntropy(e.hex) - const s = await m.toBip39Seed(salt) - Bip44Wallet.#isInternal = true - const wallet = new this(id, s, m) - await wallet.lock(passkey as string) - return wallet - } catch (err) { - throw new Error(`Error importing Bip44Wallet from entropy: ${err}`) - } - } - - /** - * Creates a new HD wallet by using a pregenerated mnemonic phrase. - * - * @param {string} password - Used to lock and unlock the wallet - * @param {string} mnemonic - Used when generating the final seed - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromMnemonic (password: string, mnemonic: string, salt?: string): Promise - /** - * Creates a new HD wallet by using a pregenerated mnemonic phrase. - * - * @param {CryptoKey} key - Used to lock and unlock the wallet - * @param {string} mnemonic - Used when generating the final seed - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromMnemonic (key: CryptoKey, mnemonic: string, salt?: string): Promise - static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string, salt: string = ''): Promise { - try { - const id = await Entropy.create(16) - const m = await Bip39Mnemonic.fromPhrase(mnemonic) - const s = await m.toBip39Seed(salt) - Bip44Wallet.#isInternal = true - const wallet = new this(id, s, m) - await wallet.lock(passkey as string) - return wallet - } catch (err) { - throw new Error(`Error importing Bip44Wallet from mnemonic: ${err}`) - } - } - - /** - * Creates a new HD wallet by using a pregenerated seed value. This seed cannot - * be used to regenerate any higher level randomness which includes entropy, - * mnemonic phrase, and salt. - * - * @param {string} password - Used to lock and unlock the wallet - * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromSeed (password: string, seed: string): Promise - /** - * Creates a new HD wallet by using a pregenerated seed value. This seed cannot - * be used to regenerate any higher level randomness which includes entropy, - * mnemonic phrase, and salt. - * - * @param {CryptoKey} key - Used to lock and unlock the wallet - * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs - * @returns {Bip44Wallet} A newly instantiated Bip44Wallet - */ - static async fromSeed (key: CryptoKey, seed: string): Promise - static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { - if (seed.length !== SEED_LENGTH_BIP44) { - throw new Error(`Expected a ${SEED_LENGTH_BIP44}-character seed, but received ${seed.length}-character string.`) - } - if (!/^[0-9a-fA-F]+$/i.test(seed)) { - throw new Error('Seed contains invalid hexadecimal characters.') - } - const id = await Entropy.create(16) - Bip44Wallet.#isInternal = true - const wallet = new this(id, seed) - await wallet.lock(passkey as string) - return wallet - } - - /** - * Retrieves an existing HD wallet from session storage using its ID. - * - * @param {string} id - Generated when the wallet was initially created - * @returns {Bip44Wallet} Restored locked Bip44Wallet - */ - static async restore (id: string): Promise { - if (typeof id !== 'string' || id === '') { - throw new TypeError('Wallet ID is required to restore') - } - Bip44Wallet.#isInternal = true - return new this(await Entropy.import(id), '') - } - - /** - * Derives BIP-44 Nano account private keys. - * - * @param {number[]} indexes - Indexes of the accounts - * @returns {Promise} - */ - async ckd (indexes: number[]): Promise { - const data: any = [] - indexes.forEach(i => data.push({ seed: this.seed, index: i })) - const privateKeys: KeyPair[] = await this.#poolBip44Ckd.assign(data) - return privateKeys - } -} - -/** -* BLAKE2b wallet created by deriving a mnemonic phrase from a seed or vice -* versa. If no value is provided for either, a new BIP-39 seed and mnemonic will -* be generated using a cryptographically strong pseudorandom number generator. -* -* Account private keys are derived on an ad hoc basis using the Blake2b hashing -* function. Account public key are derived from the private key using the -* Ed25519 key algorithm. Account addresses are derived from the public key as -* described in the Nano documentation. -* https://docs.nano.org/integration-guides/the-basics/ -* -* A password must be provided when creating or importing the wallet and is used -* to lock and unlock the wallet. The wallet will be initialized as locked. When -* the wallet is unlocked, a new password can be specified using the lock() -* method. -*/ -export class Blake2bWallet extends Wallet { - static #isInternal: boolean = false - - constructor (id: Entropy, seed: string, mnemonic?: Bip39Mnemonic) { - if (!Blake2bWallet.#isInternal) { - throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) - } - Blake2bWallet.#isInternal = false - super(id, seed, mnemonic) - } - - /** - * Creates a new BLAKE2b wallet by using a seed generated using a - * cryptographically strong pseudorandom number generator. - * - * @param {string} password - Encrypts the wallet to lock and unlock it - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async create (password: string): Promise - /** - * Creates a new BLAKE2b wallet by using a seed generated using a - * cryptographically strong pseudorandom number generator. - * - * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async create (key: CryptoKey): Promise - static async create (passkey: string | CryptoKey): Promise { - try { - const seed = await Entropy.create() - return await Blake2bWallet.fromSeed(passkey as string, seed.hex) - } catch (err) { - throw new Error(`Error creating new Blake2bWallet: ${err}`) - } - } - - /** - * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must - * ensure that it is cryptographically strongly random. - * - * @param {string} password - Used to lock and unlock the wallet - * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async fromSeed (password: string, seed: string): Promise - /** - * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must - * ensure that it is cryptographically strongly random. - * - * @param {CryptoKey} key - Used to lock and unlock the wallet - * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async fromSeed (key: CryptoKey, seed: string): Promise - static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { - if (seed.length !== SEED_LENGTH_BLAKE2B) { - throw new Error(`Expected a ${SEED_LENGTH_BLAKE2B}-character seed, but received ${seed.length}-character string.`) - } - if (!/^[0-9a-fA-F]+$/i.test(seed)) { - throw new Error('Seed contains invalid hexadecimal characters.') - } - const id = await Entropy.create(16) - const s = seed - const m = await Bip39Mnemonic.fromEntropy(seed) - Blake2bWallet.#isInternal = true - const wallet = new this(id, s, m) - await wallet.lock(passkey as string) - return wallet - } - - /** - * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. - * - * @param {string} password - Used to lock and unlock the wallet - * @param {string} mnemonic - Used when generating the final seed - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async fromMnemonic (password: string, mnemonic: string): Promise - /** - * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. - * - * @param {CryptoKey} key - Used to lock and unlock the wallet - * @param {string} mnemonic - Used when generating the final seed - * @returns {Blake2bWallet} A newly instantiated Blake2bWallet - */ - static async fromMnemonic (key: CryptoKey, mnemonic: string): Promise - static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string): Promise { - try { - const id = await Entropy.create(16) - const m = await Bip39Mnemonic.fromPhrase(mnemonic) - const s = await m.toBlake2bSeed() - Blake2bWallet.#isInternal = true - const wallet = new this(id, s, m) - await wallet.lock(passkey as string) - return wallet - } catch (err) { - throw new Error(`Error importing Blake2bWallet from mnemonic: ${err}`) - } - } - - /** - * Retrieves an existing BLAKE2b wallet from session storage using its ID. - * - * @param {string} id - Generated when the wallet was initially created - * @returns {Blake2bWallet} Restored locked Blake2bWallet - */ - static async restore (id: string): Promise { - if (typeof id !== 'string' || id === '') { - throw new TypeError('Wallet ID is required to restore') - } - Blake2bWallet.#isInternal = true - return new this(await Entropy.import(id), '') - } - - /** - * Derives BLAKE2b account private keys. - * - * @param {number[]} indexes - Indexes of the accounts - * @returns {Promise} - */ - async ckd (indexes: number[]): Promise { - const results = indexes.map(index => { - const indexHex = index.toString(16).padStart(8, '0').toUpperCase() - const inputHex = `${this.seed}${indexHex}`.padStart(72, '0') - const inputBytes = hex.toBytes(inputHex) - const privateKey: string = new Blake2b(32).update(inputBytes).digest('hex') - return { privateKey, index } - }) - return results - } -} - -/** -* Ledger hardware wallet created by communicating with a Ledger device via ADPU -* calls. This wallet does not feature any seed nor mnemonic phrase as all -* private keys are held in the secure chip of the device. As such, the user -* is responsible for using Ledger technology to back up these pieces of data. -* -* Usage of this wallet is generally controlled by calling functions of the -* `ledger` object. For example, the wallet interface should have a button to -* initiate a device connection by calling `wallet.ledger.connect()`. For more -* information, refer to the ledger.js service file. -*/ -export class LedgerWallet extends Wallet { - static #isInternal: boolean = false - #ledger: Ledger - - get ledger () { return this.#ledger } - - constructor (id: Entropy, ledger: Ledger) { - if (!LedgerWallet.#isInternal) { - throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) - } - LedgerWallet.#isInternal = false - super(id) - this.#ledger = ledger - } - - /** - * Creates a new Ledger hardware wallet communication layer by dynamically - * importing the ledger.js service. - * - * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object - */ - static async create (): Promise { - const { Ledger } = await import('../ledger') - const l = await Ledger.init() - const id = await Entropy.create(16) - LedgerWallet.#isInternal = true - return new this(id, l) - } - - /** - * Retrieves an existing Ledger wallet from session storage using its ID. - * - * @param {string} id - Generated when the wallet was initially created - * @returns {LedgerWallet} Restored LedgerWallet - */ - static async restore (id: string): Promise { - if (typeof id !== 'string' || id === '') { - throw new TypeError('Wallet ID is required to restore') - } - const { Ledger } = await import('../ledger') - const l = await Ledger.init() - LedgerWallet.#isInternal = true - return new this(await Entropy.import(id), l) - } - - /** - * Gets the public key for an account from the Ledger device. - * - * @param {number[]} indexes - Indexes of the accounts - * @returns {Promise} - */ - async ckd (indexes: number[]): Promise { - const results: KeyPair[] = [] - for (const index of indexes) { - const { status, publicKey } = await this.ledger.account(index) - if (status === 'OK' && publicKey != null) { - results.push({ publicKey, index }) - } else { - throw new Error(`Error getting Ledger account: ${status}`) - } - } - return results - } - - /** - * Attempts to close the current process on the Ledger device. - * - * Overrides the default wallet `lock()` method since as a hardware wallet it - * does not need to be encrypted by software. - * - * @returns True if successfully locked - */ - async lock (): Promise { - if (this.ledger == null) { - return false - } - const result = await this.ledger.close() - return result.status === 'OK' - } - - /** - * Attempts to connect to the Ledger device. - * - * Overrides the default wallet `unlock()` method since as a hardware wallet it - * does not need to be encrypted by software. - * - * @returns True if successfully unlocked - */ - async unlock (): Promise { - if (this.ledger == null) { - return false - } - const result = await this.ledger.connect() - return result === 'OK' - } -} diff --git a/src/lib/wallets/ledger-wallet.ts b/src/lib/wallets/ledger-wallet.ts new file mode 100644 index 0000000..f25f83f --- /dev/null +++ b/src/lib/wallets/ledger-wallet.ts @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { KeyPair, Wallet } from '.' +import { Entropy } from '#src/lib/entropy.js' +import { Ledger } from '#src/lib/ledger.js' + +/** +* Ledger hardware wallet created by communicating with a Ledger device via ADPU +* calls. This wallet does not feature any seed nor mnemonic phrase as all +* private keys are held in the secure chip of the device. As such, the user +* is responsible for using Ledger technology to back up these pieces of data. +* +* Usage of this wallet is generally controlled by calling functions of the +* `ledger` object. For example, the wallet interface should have a button to +* initiate a device connection by calling `wallet.ledger.connect()`. For more +* information, refer to the ledger.js service file. +*/ +export class LedgerWallet extends Wallet { + static #isInternal: boolean = false + #ledger: Ledger + + get ledger () { return this.#ledger } + + constructor (id: Entropy, ledger: Ledger) { + if (!LedgerWallet.#isInternal) { + throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) + } + LedgerWallet.#isInternal = false + super(id) + this.#ledger = ledger + } + + /** + * Creates a new Ledger hardware wallet communication layer by dynamically + * importing the ledger.js service. + * + * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + */ + static async create (): Promise { + const { Ledger } = await import('../ledger') + const l = await Ledger.init() + const id = await Entropy.create(16) + LedgerWallet.#isInternal = true + return new this(id, l) + } + + /** + * Retrieves an existing Ledger wallet from session storage using its ID. + * + * @param {string} id - Generated when the wallet was initially created + * @returns {LedgerWallet} Restored LedgerWallet + */ + static async restore (id: string): Promise { + if (typeof id !== 'string' || id === '') { + throw new TypeError('Wallet ID is required to restore') + } + const { Ledger } = await import('../ledger') + const l = await Ledger.init() + LedgerWallet.#isInternal = true + return new this(await Entropy.import(id), l) + } + + /** + * Gets the public key for an account from the Ledger device. + * + * @param {number[]} indexes - Indexes of the accounts + * @returns {Promise} + */ + async ckd (indexes: number[]): Promise { + const results: KeyPair[] = [] + for (const index of indexes) { + const { status, publicKey } = await this.ledger.account(index) + if (status === 'OK' && publicKey != null) { + results.push({ publicKey, index }) + } else { + throw new Error(`Error getting Ledger account: ${status}`) + } + } + return results + } + + /** + * Attempts to close the current process on the Ledger device. + * + * Overrides the default wallet `lock()` method since as a hardware wallet it + * does not need to be encrypted by software. + * + * @returns True if successfully locked + */ + async lock (): Promise { + if (this.ledger == null) { + return false + } + const result = await this.ledger.close() + return result.status === 'OK' + } + + /** + * Attempts to connect to the Ledger device. + * + * Overrides the default wallet `unlock()` method since as a hardware wallet it + * does not need to be encrypted by software. + * + * @returns True if successfully unlocked + */ + async unlock (): Promise { + if (this.ledger == null) { + return false + } + const result = await this.ledger.connect() + return result === 'OK' + } +} -- 2.47.3