From 72af7890c546b2b8d390e21db5125f720d28aba4 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 23 Jul 2025 14:14:10 -0700 Subject: [PATCH] Refactor wallet to throw if mnemonic or seed are accessed while locked. --- src/lib/wallets/bip44-wallet.ts | 2 +- src/lib/wallets/blake2b-wallet.ts | 2 +- src/lib/wallets/wallet.ts | 75 ++++++++++++++++++------------- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts index 421c024..426b2c5 100644 --- a/src/lib/wallets/bip44-wallet.ts +++ b/src/lib/wallets/bip44-wallet.ts @@ -212,7 +212,7 @@ export class Bip44Wallet extends Wallet { * @returns {Promise} */ async ckd (indexes: number[]): Promise { - if (this.seed == null) { + if (this.isLocked) { throw new Error('wallet must be unlocked to derive accounts') } const results = await Bip44CkdWorker.assign({ diff --git a/src/lib/wallets/blake2b-wallet.ts b/src/lib/wallets/blake2b-wallet.ts index 99a8f04..779dd56 100644 --- a/src/lib/wallets/blake2b-wallet.ts +++ b/src/lib/wallets/blake2b-wallet.ts @@ -161,7 +161,7 @@ export class Blake2bWallet extends Wallet { * @returns {Promise} */ async ckd (indexes: number[]): Promise { - if (this.seed == null) { + if (this.isLocked) { throw new Error('wallet must be unlocked to derive accounts') } const results = [] diff --git a/src/lib/wallets/wallet.ts b/src/lib/wallets/wallet.ts index 97898e4..1cce9c4 100644 --- a/src/lib/wallets/wallet.ts +++ b/src/lib/wallets/wallet.ts @@ -22,15 +22,21 @@ export abstract class Wallet { #accounts: AccountList #id: Entropy - #locked: boolean = true - #m: Bip39Mnemonic | null - #s: Uint8Array | null + #locked: boolean + #m?: Bip39Mnemonic + #s?: Uint8Array get id () { return `libnemo_${this.#id.hex}` } get isLocked () { return this.#locked } get isUnlocked () { return !this.#locked } - get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : null } - get seed () { return this.#s == null ? this.#s : bytes.toHex(this.#s) } + get mnemonic () { + if (this.#locked || this.#m == null) throw new Error('failed to get mnemonic', { cause: 'wallet locked' }) + return this.#m.phrase + } + get seed () { + if (this.#locked || this.#s == null) throw new Error('failed to get seed', { cause: 'wallet locked' }) + return bytes.toHex(this.#s) + } constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) { if (this.constructor === Wallet) { @@ -38,8 +44,9 @@ export abstract class Wallet { } this.#accounts = new AccountList() this.#id = id - this.#m = mnemonic ?? null - this.#s = seed ?? null + this.#locked = false + this.#m = mnemonic + this.#s = seed } /** @@ -86,7 +93,7 @@ export abstract class Wallet { * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts */ async accounts (from: number = 0, to: number = from): Promise { - if (this.seed == null) { + if (this.#locked || this.#s == null) { throw new Error('wallet must be unlocked to derive accounts') } if (from > to) { @@ -141,9 +148,10 @@ export abstract class Wallet { await this.#accounts[a].destroy() delete this.#accounts[a] } + this.#m?.destroy() bytes.erase(this.#s) - this.#s = null - this.#m = null + this.#m = undefined + this.#s = undefined await SafeWorker.assign({ store: 'Wallet', method: 'destroy', @@ -166,14 +174,14 @@ export abstract class Wallet { if (typeof password === 'string') { password = utf8.toBytes(password) } - if (password == null || !(password instanceof Uint8Array)) { - throw new Error('Failed to lock wallet') - } try { + if (password == null || !(password instanceof Uint8Array)) { + throw new Error('password must be string or bytes') + } const serialized = JSON.stringify({ - id: this.id, - mnemonic: this.mnemonic, - seed: this.seed + id: this.#id.hex, + mnemonic: this.#m?.phrase, + seed: this.#s == null ? this.#s : bytes.toHex(this.#s) }) const encoded = utf8.toBytes(serialized) const success = await SafeWorker.assign({ @@ -185,16 +193,17 @@ export abstract class Wallet { if (!success) { throw null } + this.#m?.destroy() + bytes.erase(this.#s) + this.#m = undefined + this.#s = undefined + this.#locked = true + return this.#locked } catch (err) { - throw new Error('Failed to lock wallet') + throw new Error('failed to lock wallet', { cause: err }) } finally { bytes.erase(password) } - bytes.erase(this.#s) - this.#s = null - this.#m = null - this.#locked = true - return true } /** @@ -234,19 +243,22 @@ export abstract class Wallet { if (typeof password === 'string') { password = utf8.toBytes(password) } - if (password == null || !(password instanceof Uint8Array)) { - throw new Error('Failed to unlock wallet') - } + let decoded, deserialized, id, mnemonic, seed try { + if (password == null || !(password instanceof Uint8Array)) { + throw new Error('password must be string or bytes') + } const response = await SafeWorker.assign({ method: 'fetch', name: this.id, store: 'Wallet', password: password.buffer }) - const decoded = bytes.toUtf8(new Uint8Array(response[this.id])) - const deserialized = JSON.parse(decoded) - let { id, mnemonic, seed } = deserialized + decoded = bytes.toUtf8(new Uint8Array(response[this.id])) + deserialized = JSON.parse(decoded) + id = deserialized.id + mnemonic = deserialized.mnemonic + seed = deserialized.seed if (id == null) { throw new Error('ID is null') } @@ -262,13 +274,14 @@ export abstract class Wallet { this.#s = hex.toBytes(seed) seed = null } + this.#locked = false + return true } catch (err) { - throw new Error('Failed to unlock wallet', { cause: err }) + throw new Error('failed to unlock wallet', { cause: err }) } finally { bytes.erase(password) + decoded = deserialized = id = mnemonic = seed = undefined } - this.#locked = false - return true } /** -- 2.47.3