From: Chris Duncan Date: Mon, 11 Aug 2025 15:21:52 +0000 (-0700) Subject: Extract wallet backup and restore to separate modules. Update type definition file. X-Git-Tag: v0.10.5~41^2~118 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=12e48ebb97698e97c68f8bdb4c9192b663c091aa;p=libnemo.git Extract wallet backup and restore to separate modules. Update type definition file. --- diff --git a/src/lib/wallet/backup.ts b/src/lib/wallet/backup.ts new file mode 100644 index 0000000..76752eb --- /dev/null +++ b/src/lib/wallet/backup.ts @@ -0,0 +1,35 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Database } from '../database' +import { NamedData } from '#types' +import { Wallet } from '#wallet' + +export async function _backup () { + try { + const records = await Database.getAll(Wallet.DB_NAME) + const recordIds = Object.keys(records) + return recordIds.map(recordId => { + const { id, type, iv, salt, encrypted } = records[recordId] + if (typeof id !== 'string') { + throw new TypeError('Retrieved invalid ID', { cause: id }) + } + if (type !== 'BIP-44' && type !== 'BLAKE2b') { + throw new TypeError('Retrieved invalid type', { cause: type }) + } + if (!(iv instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid iv', { cause: iv }) + } + if (!(salt instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid salt', { cause: salt }) + } + if (!(encrypted instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid encrypted data', { cause: encrypted }) + } + return { id, type, iv, salt, encrypted } as const + }) + } catch (err) { + console.error(err) + return [] + } +} diff --git a/src/lib/wallet/create.ts b/src/lib/wallet/create.ts index 3e0ef2f..e714d14 100644 --- a/src/lib/wallet/create.ts +++ b/src/lib/wallet/create.ts @@ -7,14 +7,6 @@ import { _load } from './load' import { Wallet } from '.' import { NamedData } from '#types' -/** -* 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 {Wallet} A newly instantiated Wallet -*/ export async function _create (wallet: Wallet, password: string, mnemonicSalt?: string): Promise> export async function _create (wallet: Wallet, password: unknown, mnemonicSalt?: unknown): Promise> { if (typeof password !== 'string') { diff --git a/src/lib/wallet/get.ts b/src/lib/wallet/get.ts new file mode 100644 index 0000000..9a5f1c4 --- /dev/null +++ b/src/lib/wallet/get.ts @@ -0,0 +1,31 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Database } from '../database' +import { NamedData } from '#types' +import { Wallet } from '#wallet' + +export async function _get (recordId: string) { + try { + const record = await Database.get(recordId, Wallet.DB_NAME) + const { id, type, iv, salt, encrypted } = record[recordId] + if (typeof id !== 'string') { + throw new TypeError('Retrieved invalid ID', { cause: id }) + } + if (type !== 'BIP-44' && type !== 'BLAKE2b') { + throw new TypeError('Retrieved invalid type', { cause: type }) + } + if (!(iv instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid iv', { cause: iv }) + } + if (!(salt instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid salt', { cause: salt }) + } + if (!(encrypted instanceof ArrayBuffer)) { + throw new TypeError('Retrieved invalid encrypted data', { cause: encrypted }) + } + return { id, type, iv, salt, encrypted } as const + } catch (err) { + throw new Error('Failed to get wallet from database', { cause: err }) + } +} diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts index b03275a..56dbc28 100644 --- a/src/lib/wallet/index.ts +++ b/src/lib/wallet/index.ts @@ -3,12 +3,15 @@ import { KeyPair, NamedData, WalletType } from '#types' import { Account, AccountList } from '../account' +import { _backup } from './backup' import { Block } from '../block' import { ADDRESS_GAP } from '../constants' import { bytes, hex, utf8 } from '../convert' import { _create } from './create' import { Database } from '../database' +import { _get } from './get' import { _load } from './load' +import { _restore } from './restore' import { Rpc } from '../rpc' import { default as VaultWorker } from '../vault/vault' import { WorkerQueue } from '../vault/worker-queue' @@ -22,18 +25,6 @@ export class Wallet { static #isInternal: boolean = false static DB_NAME = 'Wallet' - /** - * Retrieves a wallet from the database. - */ - static async #get (id: string) { - try { - const record = await Database.get(id, this.DB_NAME) - return record[id] - } catch (err) { - throw new Error('Failed to get wallet from database', { cause: err }) - } - } - /** * Creates a new HD wallet by using an entropy value generated using a * cryptographically strong pseudorandom number generator. @@ -50,19 +41,12 @@ export class Wallet { } /** - * Retrieves all wallet IDs from the database. + * Retrieves all encrypted wallets from the database. * - * @returns Array of hexadecimal-formatted wallet IDs + * @returns Array of wallets with encrypted secrets and unencrypted metadata */ - static async export (): Promise { - try { - const response = await Database.getAll(this.DB_NAME) - const ids = Object.keys(response) - return ids.map(id => response[id]) - } catch (err) { - console.error(err) - return [] - } + static async backup (): Promise { + return _backup() } /** @@ -94,26 +78,26 @@ export class Wallet { } /** - * Retrieves an existing wallet from the database using its UUID. + * Instantiates a Wallet from an existing record in the database using its UUID. * * @param {string} id - Generated when the wallet was created or imported - * @returns {Wallet} Restored locked Wallet + * @returns {Promise} Restored locked Wallet */ - static async restore (id: string): Promise { - try { - if (typeof id !== 'string' || id === '') { - throw new TypeError('Wallet ID is required to restore') - } - const { type } = await this.#get(id) - if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Ledger') { - throw new Error('Invalid wallet type from database') - } + static async restore (id: string): Promise + /** + * Instantiates Wallet objects from records in the database. + * + * @param {string} id - Generated when the wallet was created or imported + * @returns {Promise} Restored locked Wallets + */ + static async restore (): Promise + static async restore (id?: string): Promise { + const backups = await _restore(id) + const wallets = backups.map(backup => { Wallet.#isInternal = true - const self = new this(type, id) - return self - } catch (err) { - throw new Error('Failed to restore wallet', { cause: err }) - } + return new this(backup.type, backup.id) + }) + return typeof id === 'string' ? wallets[0] : wallets } #accounts: AccountList @@ -264,8 +248,8 @@ export class Wallet { } /** - * Removes encrypted secrets in storage and releases variable references to - * allow garbage collection. + * Removes encrypted secrets in storage, releases variable references to + * allow garbage collection, and terminates vault worker. */ async destroy (): Promise { try { @@ -371,7 +355,7 @@ export class Wallet { if (typeof password !== 'string') { throw new TypeError('Password must be a string') } - const { iv, salt, encrypted } = await Wallet.#get(this.#id) + const { iv, salt, encrypted } = await _get(this.#id) const { isUnlocked } = await this.#vault.request({ action: 'unlock', type: this.#type, diff --git a/src/lib/wallet/load.ts b/src/lib/wallet/load.ts index b4d7bad..9be9302 100644 --- a/src/lib/wallet/load.ts +++ b/src/lib/wallet/load.ts @@ -7,14 +7,6 @@ import { hex, utf8 } from '../convert' import { Database } from '../database' import { Wallet } from '.' -/** -* Imports an existing HD wallet by using an entropy value generated using a -* cryptographically strong pseudorandom number generator.NamedD -* -* @param {string} password - Encrypts the wallet to lock and unlock it -* @param {string} [salt=''] - Used when generating the final seed -* @returns {Wallet} A newly instantiated Wallet -*/ export async function _load (wallet: Wallet, password: string, secret: string, mnemonicSalt?: string): Promise export async function _load (wallet: Wallet, password: unknown, secret: unknown, mnemonicSalt?: unknown): Promise { if (typeof password !== 'string') { diff --git a/src/lib/wallet/restore.ts b/src/lib/wallet/restore.ts new file mode 100644 index 0000000..849dfd5 --- /dev/null +++ b/src/lib/wallet/restore.ts @@ -0,0 +1,29 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { _backup } from './backup' +import { _get } from './get' +import { WalletType } from '#types' + +export async function _restore (id?: unknown) { + try { + if (typeof id !== undefined && typeof id !== 'string') { + throw new TypeError('ID to restore must be a string') + } + const wallets = [] + const backups = [] + if (typeof id === 'string') { + backups.push(await _get(id)) + } + if (id === undefined) { + backups.push(...(await _backup())) + } + for (const backup of backups) { + wallets.push(backup) + } + return wallets + } catch (err) { + throw new Error('Failed to restore wallet', { cause: err }) + } +} + diff --git a/src/types.d.ts b/src/types.d.ts index a7a3767..1a96520 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -193,7 +193,6 @@ export declare class Bip39 { */ toBlake2bSeed (format: 'hex'): Promise } - /** * Implementation derived from blake2b@2.1.4. Copyright 2017 Emil Bay * (https://github.com/emilbayes/blake2b). See LICENSES/ISC.txt @@ -315,7 +314,8 @@ export declare class Block { /** * Sets the `signature` property of the block to a precalculated value. * - * @param {string} [key] - 64-byte hexadecimal signature + * @param {string} signature - 64-byte hexadecimal signature + * @returns Block with `signature` value set */ sign (signature: string): Block /** @@ -323,18 +323,10 @@ export declare class Block { * the `signature` property of the block. * * @param {string} [key] - 32-byte hexadecimal private key to use for signing + * @returns Block with `signature` value set */ sign (key: string): Promise /** - * Signs the block using a Wallet. If successful, the result is stored in - * the `signature` property of the block. The wallet must be unlocked prior to - * signing. - * - * @param {Wallet} wallet - Wallet to use for signing - * @param {number} index - Account in wallet to use for signing - */ - sign (wallet: Wallet, index: number): Promise - /** * Signs the block using a Ledger hardware wallet. If that fails, an error is * thrown with the status code from the device. If successful, the result is * stored in the `signature` property of the block. The wallet must be unlocked @@ -342,9 +334,20 @@ export declare class Block { * * @param {number} index - Account index between 0x0 and 0x7fffffff * @param {object} [frontier] - JSON of frontier block for offline signing + * @returns Block with `signature` value set */ sign (index: number, frontier?: Block): Promise /** + * Signs the block using a Wallet. If successful, the result is stored in + * the `signature` property of the block. The wallet must be unlocked prior to + * signing. + * + * @param {Wallet} wallet - Wallet to use for signing + * @param {number} index - Account in wallet to use for signing + * @returns Block with `signature` value set + */ + sign (wallet: Wallet, index: number): Promise + /** * Verifies the signature of the block. If a key is not provided, the public * key of the block's account will be used if it exists. * @@ -356,55 +359,6 @@ export declare class Block { export type Data = boolean | number | number[] | string | string[] | ArrayBuffer | CryptoKey | { [key: string]: Data } -/** -* Represents a cryptographically strong source of entropy suitable for use in -* BIP-39 mnemonic phrase generation and consequently BIP-44 key derivation. -* -* The constructor will accept one of several different data types under certain -* constraints. If the constraints are not met, an error will be thrown. If no -* value, or the equivalent of no value, is passed to the constructor, then a -* brand new source of entropy will be generated at the maximum size of 256 bits. -*/ -export declare class Entropy { - #private - static MIN: 16 - static MAX: 32 - static MOD: 4 - get bits (): string - get buffer (): ArrayBuffer - get bytes (): Uint8Array - get hex (): string - private constructor () - /** - * Generate 256 bits of entropy. - */ - static create (): Promise - /** - * Generate between 16-32 bytes of entropy. - * @param {number} size - Number of bytes to generate in multiples of 4 - */ - static create (size: number): Promise - /** - * Import existing entropy and validate it. - * @param {string} hex - Hexadecimal string - */ - static import (hex: string): Promise - /** - * Import existing entropy and validate it. - * @param {ArrayBuffer} buffer - Byte buffer - */ - static import (buffer: ArrayBuffer): Promise - /** - * Import existing entropy and validate it. - * @param {Uint8Array} bytes - Byte array - */ - static import (bytes: Uint8Array): Promise - /** - * Randomizes the bytes, rendering the original values generally inaccessible. - */ - destroy (): boolean -} - export type NamedData = { [key: string]: T } @@ -519,11 +473,12 @@ type SweepResult = { /** * Converts a decimal amount of nano from one unit divider to another. * -* @param {bigint|string} amount - Decimal amount to convert +* @param {(bigint|number|string)} amount - Decimal amount to convert * @param {string} inputUnit - Current denomination * @param {string} outputUnit - Desired denomination */ -export declare function convert (amount: bigint | string, inputUnit: string, outputUnit: string): Promise +export declare function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string): string +export declare function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format?: 'bigint' | 'string'): bigint declare function hash (data: string | string[], encoding?: 'hex'): Uint8Array /** * Signs arbitrary strings with a private key using the Ed25519 signature scheme. @@ -582,36 +537,46 @@ export declare class Wallet { */ static create (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise /** - * Retrieves all wallet IDs from the database. + * Retrieves all encrypted wallets from the database. * - * @returns Array of hexadecimal-formatted wallet IDs + * @returns Array of wallets with encrypted secrets and unencrypted metadata */ - static export (): Promise + static backup (): Promise /** * Imports an existing HD wallet by using an entropy value generated using a * cryptographically strong pseudorandom number generator.NamedD * - * @param {string} password - Encrypts the wallet to lock and unlock it - * @param {string} [salt=''] - Used when generating the final seed - * @returns {Wallet} A newly instantiated Wallet + * @param {string} type - Algorithm used to generate wallet and child accounts + * @param {string} password - Encrypts the wallet to lock and unlock it. Discard as soon as possible after loading the wallet. + * @param {string} seed - Used to derive child accounts + * @returns Wallet in a locked state */ - static import (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise + static load (type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise /** * Imports an existing 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 {Wallet} A newly instantiated Wallet + * @param {string} type - Algorithm used to generate wallet and child accounts + * @param {string} password - Encrypts the wallet to lock and unlock it. Discard as soon as possible after loading the wallet. + * @param {string} mnemonicPhrase - Used to derive the wallet seed + * @param {string} [mnemonicSalt] - Used to alter the seed derived from the mnemonic phrase + * @returns Wallet in a locked state */ - static import (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise + static load (type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise /** - * Retrieves an existing wallet from the database using its UUID. + * Instantiates a Wallet from an existing record in the database using its UUID. * * @param {string} id - Generated when the wallet was created or imported - * @returns {Wallet} Restored locked Wallet + * @returns {Promise} Restored locked Wallet */ static restore (id: string): Promise + /** + * Instantiates Wallet objects from records in the database. + * + * @param {string} id - Generated when the wallet was created or imported + * @returns {Promise} Restored locked Wallets + */ + static restore (): Promise get id (): string get vault (): WorkerQueue get type (): WalletType @@ -624,17 +589,19 @@ export declare class Wallet { * Retrieves an account from a wallet using its child key derivation function. * Defaults to the first account at index 0. * + * The returned object will have keys corresponding with the requested range + * of account indexes. The value of each key will be the Account derived for + * that index in the wallet. + * * ``` - * console.log(await wallet.account(5)) - * // outputs sixth account of the wallet - * // { - * // privateKey: <...>, - * // index: 5 - * // } + * const account = await wallet.account(1) + * // outputs the second account of the wallet + * console.log(account) + * // { address: <...>, publicKey: <...>, index: 1, } * ``` * - * @param {number} index - Wallet index of secret key. Default: 0 - * @returns {Account} Account derived at the specified wallet index + * @param {number} index - Wallet index of account. Default: 0 + * @returns Promise for the Account at the specified index */ account (index?: number): Promise /** @@ -646,24 +613,37 @@ export declare class Wallet { * that index in the wallet. * * ``` - * console.log(await wallet.accounts(5)) - * // outputs sixth account of the wallet + * const accounts = await wallet.accounts(0, 1)) + * // outputs the first and second account of the wallet + * console.log(accounts) * // { - * // 5: { - * // privateKey: <...>, - * // index: 5 - * // } + * // 0: { + * // address: <...>, + * // publicKey: <...>, + * // index: 0, + * // + * // }, + * // 1: { + * // address: <...>, + * // publicKey: <...>, + * // index: 1, + * // + * // } * // } + * // individual accounts can be referenced using array index notation + * console.log(accounts[1]) + * // { address: <...>, publicKey: <...>, index: 1, } * ``` * - * @param {number} from - Start index of secret keys. Default: 0 - * @param {number} to - End index of secret keys. Default: `from` - * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts + * If `from` is greater than `to`, their values will be swapped. + * @param {number} from - Start index of accounts. Default: 0 + * @param {number} to - End index of accounts. Default: `from` + * @returns {AccountList} Promise for a list of Accounts at the specified indexes */ accounts (from?: number, to?: number): Promise /** - * Removes encrypted secrets in storage and releases variable references to - * allow garbage collection. + * Removes encrypted secrets in storage, releases variable references to + * allow garbage collection, and terminates vault worker. */ destroy (): Promise /** @@ -690,19 +670,8 @@ export declare class Wallet { * * @param {number} index - Account to use for signing * @param {(Block)} block - Block data to be hashed and signed - * @returns {Promise} Hexadecimal-formatted 64-byte signature */ - sign (index: number, block: Block): Promise> - /** - * Signs a block using the private key of the account at the wallet index - * specified. The signature is appended to the signature field of the block - * before being returned. The wallet must be unlocked prior to signing. - * - * @param {number} index - Account to use for signing - * @param {(Block)} block - Block data to be hashed and signed - * @returns {Promise} Hexadecimal-formatted 64-byte signature - */ - sign (index: number, block: Block, format: 'hex'): Promise + sign (index: number, block: Block): Promise /** * Unlocks the wallet using the same password as used prior to lock it. * @@ -769,13 +738,7 @@ interface LedgerSignResponse extends LedgerResponse { export declare class Ledger extends Wallet { #private static DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID - static get listenTimeout (): 30000 - static get openTimeout (): 3000 - /** - * Check which transport protocols are supported by the browser and return the - * transport type according to the following priorities: Bluetooth, USB, HID. - */ - static get isUnsupported (): boolean + static SYMBOL: Symbol /** * Creates a new Ledger hardware wallet communication layer by dynamically * importing the ledger.js service. @@ -788,17 +751,17 @@ export declare class Ledger extends Wallet { * be extracted from the device. */ static import (): Promise - get status (): DeviceStatus private constructor () + get status (): DeviceStatus /** - * Gets the public key for an account from the Ledger device. + * Gets the index and public key for an account from the Ledger device. * - * @param {number[]} indexes - Indexes of the accounts - * @returns {Promise} + * @param {number} index - Wallet index of the account + * @returns Promise for the Account at the index specified */ account (index: number): Promise /** - * Retrieves accounts from a wallet using its child key derivation function. + * Retrieves accounts from a Ledger wallet using its internal secure software. * Defaults to the first account at index 0. * * The returned object will have keys corresponding with the requested range @@ -806,19 +769,32 @@ export declare class Ledger extends Wallet { * that index in the wallet. * * ``` - * console.log(await wallet.accounts(5)) - * // outputs sixth account of the wallet + * const accounts = await wallet.accounts(0, 1)) + * // outputs the first and second account of the wallet + * console.log(accounts) * // { - * // 5: { - * // privateKey: <...>, - * // index: 5 - * // } + * // 0: { + * // address: <...>, + * // publicKey: <...>, + * // index: 0, + * // + * // }, + * // 1: { + * // address: <...>, + * // publicKey: <...>, + * // index: 1, + * // + * // } * // } + * // individual accounts can be referenced using array index notation + * console.log(accounts[1]) + * // { address: <...>, publicKey: <...>, index: 1, } * ``` * - * @param {number} from - Start index of secret keys. Default: 0 - * @param {number} to - End index of secret keys. Default: `from` - * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts + * If `from` is greater than `to`, their values will be swapped. + * @param {number} from - Start index of accounts. Default: 0 + * @param {number} to - End index of accounts. Default: `from` + * @returns {AccountList} Promise for a list of Accounts at the specified indexes */ accounts (from?: number, to?: number): Promise /** @@ -849,24 +825,14 @@ export declare class Ledger extends Wallet { * @returns True if successfully locked */ lock (): Promise - onConnectUsb: (e: USBConnectionEvent) => Promise - onDisconnectUsb: (e: USBConnectionEvent) => Promise - /** - * Sign a block with the Ledger device. - * - * @param {number} index - Account number - * @param {object} block - Block data to sign - * @returns {Promise} Signature - */ - sign (index: number, block: Block): Promise> /** * Sign a block with the Ledger device. * * @param {number} index - Account number - * @param {object} block - Block data to sign - * @returns {Promise} Signature + * @param {Block} block - Block data to sign + * @param {Block} [frontier] - Previous block data to cache in the device */ - sign (index: number, block: Block, format?: 'hex', frontier?: Block): Promise + sign (index: number, block: Block, frontier?: Block): Promise /** * Attempts to connect to the Ledger device. *