]> git.codecow.com Git - libnemo.git/commitdiff
Refactor wallet to throw if mnemonic or seed are accessed while locked.
authorChris Duncan <chris@zoso.dev>
Wed, 23 Jul 2025 21:14:10 +0000 (14:14 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 23 Jul 2025 21:14:10 +0000 (14:14 -0700)
src/lib/wallets/bip44-wallet.ts
src/lib/wallets/blake2b-wallet.ts
src/lib/wallets/wallet.ts

index 421c024a6bea8ecf2e64367bc1865583689854e1..426b2c5f22b5beab5e453dcf54fef0e5e8b17e7a 100644 (file)
@@ -212,7 +212,7 @@ export class Bip44Wallet extends Wallet {
        * @returns {Promise<Account>}\r
        */\r
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
-               if (this.seed == null) {\r
+               if (this.isLocked) {\r
                        throw new Error('wallet must be unlocked to derive accounts')\r
                }\r
                const results = await Bip44CkdWorker.assign({\r
index 99a8f04a8e467140a2d847570064d9e7ab4aaadf..779dd562f00af73608a3ca5fdf85882c687c3ef7 100644 (file)
@@ -161,7 +161,7 @@ export class Blake2bWallet extends Wallet {
        * @returns {Promise<Account>}\r
        */\r
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
-               if (this.seed == null) {\r
+               if (this.isLocked) {\r
                        throw new Error('wallet must be unlocked to derive accounts')\r
                }\r
                const results = []\r
index 97898e4a7814712d732796176005d53de9a7b83d..1cce9c4cdaafb4394e34846233b3966c340c8846 100644 (file)
@@ -22,15 +22,21 @@ export abstract class Wallet {
 \r
        #accounts: AccountList\r
        #id: Entropy\r
-       #locked: boolean = true\r
-       #m: Bip39Mnemonic | null\r
-       #s: Uint8Array<ArrayBuffer> | null\r
+       #locked: boolean\r
+       #m?: Bip39Mnemonic\r
+       #s?: Uint8Array<ArrayBuffer>\r
 \r
        get id () { return `libnemo_${this.#id.hex}` }\r
        get isLocked () { return this.#locked }\r
        get isUnlocked () { return !this.#locked }\r
-       get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : null }\r
-       get seed () { return this.#s == null ? this.#s : bytes.toHex(this.#s) }\r
+       get mnemonic () {\r
+               if (this.#locked || this.#m == null) throw new Error('failed to get mnemonic', { cause: 'wallet locked' })\r
+               return this.#m.phrase\r
+       }\r
+       get seed () {\r
+               if (this.#locked || this.#s == null) throw new Error('failed to get seed', { cause: 'wallet locked' })\r
+               return bytes.toHex(this.#s)\r
+       }\r
 \r
        constructor (id: Entropy, seed?: Uint8Array<ArrayBuffer>, mnemonic?: Bip39Mnemonic) {\r
                if (this.constructor === Wallet) {\r
@@ -38,8 +44,9 @@ export abstract class Wallet {
                }\r
                this.#accounts = new AccountList()\r
                this.#id = id\r
-               this.#m = mnemonic ?? null\r
-               this.#s = seed ?? null\r
+               this.#locked = false\r
+               this.#m = mnemonic\r
+               this.#s = seed\r
        }\r
 \r
        /**\r
@@ -86,7 +93,7 @@ export abstract class Wallet {
        * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts\r
        */\r
        async accounts (from: number = 0, to: number = from): Promise<AccountList> {\r
-               if (this.seed == null) {\r
+               if (this.#locked || this.#s == null) {\r
                        throw new Error('wallet must be unlocked to derive accounts')\r
                }\r
                if (from > to) {\r
@@ -141,9 +148,10 @@ export abstract class Wallet {
                                await this.#accounts[a].destroy()\r
                                delete this.#accounts[a]\r
                        }\r
+                       this.#m?.destroy()\r
                        bytes.erase(this.#s)\r
-                       this.#s = null\r
-                       this.#m = null\r
+                       this.#m = undefined\r
+                       this.#s = undefined\r
                        await SafeWorker.assign<boolean>({\r
                                store: 'Wallet',\r
                                method: 'destroy',\r
@@ -166,14 +174,14 @@ export abstract class Wallet {
                if (typeof password === 'string') {\r
                        password = utf8.toBytes(password)\r
                }\r
-               if (password == null || !(password instanceof Uint8Array)) {\r
-                       throw new Error('Failed to lock wallet')\r
-               }\r
                try {\r
+                       if (password == null || !(password instanceof Uint8Array)) {\r
+                               throw new Error('password must be string or bytes')\r
+                       }\r
                        const serialized = JSON.stringify({\r
-                               id: this.id,\r
-                               mnemonic: this.mnemonic,\r
-                               seed: this.seed\r
+                               id: this.#id.hex,\r
+                               mnemonic: this.#m?.phrase,\r
+                               seed: this.#s == null ? this.#s : bytes.toHex(this.#s)\r
                        })\r
                        const encoded = utf8.toBytes(serialized)\r
                        const success = await SafeWorker.assign({\r
@@ -185,16 +193,17 @@ export abstract class Wallet {
                        if (!success) {\r
                                throw null\r
                        }\r
+                       this.#m?.destroy()\r
+                       bytes.erase(this.#s)\r
+                       this.#m = undefined\r
+                       this.#s = undefined\r
+                       this.#locked = true\r
+                       return this.#locked\r
                } catch (err) {\r
-                       throw new Error('Failed to lock wallet')\r
+                       throw new Error('failed to lock wallet', { cause: err })\r
                } finally {\r
                        bytes.erase(password)\r
                }\r
-               bytes.erase(this.#s)\r
-               this.#s = null\r
-               this.#m = null\r
-               this.#locked = true\r
-               return true\r
        }\r
 \r
        /**\r
@@ -234,19 +243,22 @@ export abstract class Wallet {
                if (typeof password === 'string') {\r
                        password = utf8.toBytes(password)\r
                }\r
-               if (password == null || !(password instanceof Uint8Array)) {\r
-                       throw new Error('Failed to unlock wallet')\r
-               }\r
+               let decoded, deserialized, id, mnemonic, seed\r
                try {\r
+                       if (password == null || !(password instanceof Uint8Array)) {\r
+                               throw new Error('password must be string or bytes')\r
+                       }\r
                        const response = await SafeWorker.assign<ArrayBuffer>({\r
                                method: 'fetch',\r
                                name: this.id,\r
                                store: 'Wallet',\r
                                password: password.buffer\r
                        })\r
-                       const decoded = bytes.toUtf8(new Uint8Array(response[this.id]))\r
-                       const deserialized = JSON.parse(decoded)\r
-                       let { id, mnemonic, seed } = deserialized\r
+                       decoded = bytes.toUtf8(new Uint8Array(response[this.id]))\r
+                       deserialized = JSON.parse(decoded)\r
+                       id = deserialized.id\r
+                       mnemonic = deserialized.mnemonic\r
+                       seed = deserialized.seed\r
                        if (id == null) {\r
                                throw new Error('ID is null')\r
                        }\r
@@ -262,13 +274,14 @@ export abstract class Wallet {
                                this.#s = hex.toBytes(seed)\r
                                seed = null\r
                        }\r
+                       this.#locked = false\r
+                       return true\r
                } catch (err) {\r
-                       throw new Error('Failed to unlock wallet', { cause: err })\r
+                       throw new Error('failed to unlock wallet', { cause: err })\r
                } finally {\r
                        bytes.erase(password)\r
+                       decoded = deserialized = id = mnemonic = seed = undefined\r
                }\r
-               this.#locked = false\r
-               return true\r
        }\r
 \r
        /**\r