From: Chris Duncan Date: Tue, 9 Sep 2025 19:32:22 +0000 (-0700) Subject: Implement notification to wallet of vault lock or unlock. X-Git-Tag: v0.10.5~22^2~13 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=39f86a493faeef63d1f20e5d3ba0e0307096472e;p=libnemo.git Implement notification to wallet of vault lock or unlock. --- diff --git a/src/lib/vault/index.ts b/src/lib/vault/index.ts index 646fad6..d7636e9 100644 --- a/src/lib/vault/index.ts +++ b/src/lib/vault/index.ts @@ -21,6 +21,7 @@ export class Vault { #job?: Task #isIdle: boolean + #isLocked: boolean #isTerminated: boolean #queue: Task[] = [] #url: string @@ -28,6 +29,7 @@ export class Vault { constructor () { this.#isIdle = true + this.#isLocked = true this.#isTerminated = false this.#queue = [] this.#url = URL.createObjectURL(new Blob([blob], { type: 'text/javascript' })) @@ -46,6 +48,8 @@ export class Vault { Vault.#instances.push(this) } + get isLocked (): boolean { return this.#isLocked } + request (data: NamedData): Promise> { return new Promise((resolve, reject): void => { if (this.#isTerminated) { @@ -89,12 +93,16 @@ export class Vault { } #report (results: any): void { + if (results === 'locked' || results === 'unlocked') { + this.#isLocked = results === 'locked' + return + } if (this.#job == null) { throw new Error('Worker returned results but had nowhere to report it.') } const { resolve, reject } = this.#job try { - if (results.error != null) { + if (results?.error != null) { reject(results) } else { resolve(results) diff --git a/src/lib/vault/vault-timer.ts b/src/lib/vault/vault-timer.ts index 96af34a..35efdd1 100644 --- a/src/lib/vault/vault-timer.ts +++ b/src/lib/vault/vault-timer.ts @@ -6,16 +6,19 @@ export class VaultTimer { #elapsed: number = 0 #isPaused: boolean = false #start: number + #ticker: number | NodeJS.Timeout #timeout: number | NodeJS.Timeout constructor (f: () => any, t: number) { + this.#ticker = setInterval(() => { }, 1000) this.#f = f this.#start = performance.now() - this.#timeout = setTimeout(f, t) + this.#timeout = setTimeout(this.#f, t) } pause () { if (!this.#isPaused) { + clearInterval(this.#ticker) clearTimeout(this.#timeout) this.#elapsed = performance.now() - this.#start this.#isPaused = true @@ -24,6 +27,7 @@ export class VaultTimer { resume () { if (this.#isPaused) { + this.#ticker = setInterval(() => { }, 1000) this.#start = performance.now() this.#timeout = setTimeout(this.#f, this.#elapsed) this.#isPaused = false diff --git a/src/lib/vault/vault-worker.ts b/src/lib/vault/vault-worker.ts index 5cce665..9187a77 100644 --- a/src/lib/vault/vault-worker.ts +++ b/src/lib/vault/vault-worker.ts @@ -32,7 +32,7 @@ export class VaultWorker { const action = this.#parseAction(data) const keySalt = this.#parseKeySalt(action, data) Passkey.create(action, keySalt, data) - .then((key: CryptoKey | undefined): Promise => { + .then((key: CryptoKey | undefined): Promise => { const type = this.#parseType(action, data) const iv = this.#parseIv(action, data) const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message } = this.#extractData(action, data) @@ -72,9 +72,11 @@ export class VaultWorker { }) .then(result => { const transfer = [] - for (const k of Object.keys(result)) { - if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) { - transfer.push(result[k]) + if (result) { + for (const k of Object.keys(result)) { + if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) { + transfer.push(result[k]) + } } } //@ts-expect-error @@ -183,12 +185,13 @@ export class VaultWorker { .finally(() => this.lock()) } - lock (): NamedData { + lock (): void { this.#mnemonic = undefined this.#seed = undefined this.#locked = true this.#timeout?.pause() - return { isLocked: this.#locked } + BROWSER: postMessage('locked') + NODE: this.#parentPort?.postMessage('locked') } /** @@ -228,7 +231,7 @@ export class VaultWorker { /** * Decrypts the input and sets the seed and, if it is included, the mnemonic. */ - unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise> { + unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise { if (type == null) { throw new TypeError('Wallet type is required') } @@ -253,8 +256,9 @@ export class VaultWorker { this.#seed = seed this.#mnemonic = mnemonic this.#locked = false - this.#timeout = new VaultTimer(() => this.lock(), 120000) - return { isUnlocked: !this.#locked } + this.#timeout = new VaultTimer(this.lock.bind(this), 120000) + BROWSER: postMessage('unlocked') + NODE: this.#parentPort?.postMessage('unlocked') }) .catch(err => { console.error(err) diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts index 6f5ecfe..751e47d 100644 --- a/src/lib/wallet/index.ts +++ b/src/lib/wallet/index.ts @@ -135,6 +135,13 @@ export class Wallet { */ get id (): string { return this.#id } + /** + * @returns True if the wallet is locked, else false + */ + get isLocked (): boolean { + return this.#vault.isLocked + } + /** * Algorithm or device used to create wallet and derive accounts. */ diff --git a/src/lib/wallet/lock.ts b/src/lib/wallet/lock.ts index 3f3e3ca..7d09220 100644 --- a/src/lib/wallet/lock.ts +++ b/src/lib/wallet/lock.ts @@ -17,10 +17,10 @@ export async function _lock (wallet: Wallet, vault: Vault): Promise { } }) } else { - const { isLocked } = await vault.request({ + await vault.request({ action: 'lock' }) - if (!isLocked) { + if (!wallet.isLocked) { throw new Error('Lock request to Vault failed') } } diff --git a/src/lib/wallet/unlock.ts b/src/lib/wallet/unlock.ts index f46e3fd..c53d5fd 100644 --- a/src/lib/wallet/unlock.ts +++ b/src/lib/wallet/unlock.ts @@ -20,7 +20,7 @@ export async function _unlock (wallet: Wallet, vault: Vault, password: unknown): throw new TypeError('Password must be a string') } const { iv, salt, encrypted } = await _get(wallet.id) - const { isUnlocked } = await vault.request({ + await vault.request({ action: 'unlock', type: wallet.type, password: utf8.toBuffer(password), @@ -28,7 +28,7 @@ export async function _unlock (wallet: Wallet, vault: Vault, password: unknown): keySalt: salt, encrypted }) - if (!isUnlocked) { + if (wallet.isLocked) { throw new Error('Unlock request to Vault failed') } } diff --git a/src/types.d.ts b/src/types.d.ts index d4f776d..4d2d321 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -667,6 +667,10 @@ export declare class Wallet { */ get id (): string /** + * @returns True if the wallet is locked, else false + */ + get isLocked (): boolean + /** * Algorithm or device used to create wallet and derive accounts. */ get type (): WalletType diff --git a/test/test.lock-unlock.mjs b/test/test.lock-unlock.mjs index 14386df..1618ad9 100644 --- a/test/test.lock-unlock.mjs +++ b/test/test.lock-unlock.mjs @@ -138,9 +138,12 @@ await Promise.all([ await assert.resolves(wallet.destroy()) }) - await test('wallet automatic lock resets after user activity', { skip: true }, async () => { + await test('wallet automatic lock resets after user activity', { skip: false }, async () => { + console.log('Starting autolock test...') const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + assert.equal(wallet.isLocked, true) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + assert.equal(wallet.isLocked, false) assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) @@ -149,6 +152,7 @@ await Promise.all([ console.log('Waiting 1 minute...') setTimeout(async () => { // should still be unlocked + assert.equal(wallet.isLocked, false) const account = await wallet.account(0) assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) resolve(null) @@ -159,6 +163,7 @@ await Promise.all([ console.log('Timer should be reset, waiting 1 minute...') setTimeout(async () => { // should still be unlocked from account() reset and not initial unlock + assert.equal(wallet.isLocked, false) assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) resolve(null) @@ -169,6 +174,7 @@ await Promise.all([ console.log('Timer should not be reset by verify, waiting 1 minute...') setTimeout(async () => { // should be locked from account() reset and not reset by verify() + assert.equal(wallet.isLocked, true) await assert.rejects(wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) await assert.rejects(wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) await assert.resolves(wallet.destroy())