From: Chris Duncan Date: Mon, 18 Aug 2025 19:15:32 +0000 (-0700) Subject: Create timer for vault locking. Create unit test. X-Git-Tag: v0.10.5~41^2~64 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=1cfba1a5e6a8f26f882dc415223eb2b2c2fc8a22;p=libnemo.git Create timer for vault locking. Create unit test. --- diff --git a/src/lib/vault/timer.ts b/src/lib/vault/timer.ts new file mode 100644 index 0000000..d788ad4 --- /dev/null +++ b/src/lib/vault/timer.ts @@ -0,0 +1,32 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +export class Timer { + #f: () => any + #elapsed: number = 0 + #isPaused: boolean = false + #start: number + #timeout: number | NodeJS.Timeout + + constructor (f: () => any, t: number) { + this.#f = f + this.#start = performance.now() + this.#timeout = setTimeout(f, t) + } + + pause () { + if (!this.#isPaused) { + clearTimeout(this.#timeout) + this.#elapsed = performance.now() - this.#start + this.#isPaused = true + } + } + + resume () { + if (this.#isPaused) { + this.#start = performance.now() + this.#timeout = setTimeout(this.#f, this.#elapsed) + this.#isPaused = false + } + } +} diff --git a/src/lib/vault/vault.ts b/src/lib/vault/vault.ts index d6b45b5..fcf1591 100644 --- a/src/lib/vault/vault.ts +++ b/src/lib/vault/vault.ts @@ -6,13 +6,14 @@ import { Bip39, Bip44, Blake2b, NanoNaCl } from '#crypto' import { NamedData } from '#types' import { default as Constants, BIP44_COIN_NANO } from '../constants' import { default as Convert, utf8 } from '../convert' +import { Timer } from './timer' /** * Cross-platform worker for managing wallet secrets. */ export class Vault { static #locked: boolean = true - static #timeout: number | NodeJS.Timeout + static #timeout: Timer static #type?: 'BIP-44' | 'BLAKE2b' static #seed?: ArrayBuffer static #mnemonic?: ArrayBuffer @@ -120,6 +121,7 @@ export class Vault { } return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() } } catch (err) { + console.error(err) throw new Error('Failed to create wallet', { cause: err }) } finally { this.lock() @@ -145,14 +147,16 @@ export class Vault { if (typeof index !== 'number') { throw new Error('Invalid wallet account index') } - clearTimeout(this.#timeout) + this.#timeout.pause() const prv = this.#type === 'BIP-44' ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) : await this.#deriveBlake2bPrivateKey(this.#seed, index) const pub = await NanoNaCl.convert(new Uint8Array(prv)) - this.#timeout = setTimeout(() => this.lock(), 300000) + this.#timeout = new Timer(() => this.lock(), 300000) return { index, publicKey: pub.buffer } } catch (err) { + console.error(err) + this.#timeout.resume() throw new Error('Failed to derive account', { cause: err }) } } @@ -169,6 +173,7 @@ export class Vault { } return record } catch (err) { + console.error(err) throw new Error('Failed to load wallet', { cause: err }) } finally { this.lock() @@ -176,7 +181,7 @@ export class Vault { } static lock (): NamedData { - clearTimeout(this.#timeout) + this.#timeout?.pause() this.#mnemonic = undefined this.#seed = undefined this.#locked = true @@ -201,14 +206,16 @@ export class Vault { if (data == null) { throw new Error('Data to sign not found') } - clearTimeout(this.#timeout) + this.#timeout.pause() const prv = this.#type === 'BIP-44' ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index) : await this.#deriveBlake2bPrivateKey(this.#seed, index) const sig = await NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv)) - this.#timeout = setTimeout(() => this.lock(), 300000) + this.#timeout = new Timer(() => this.lock(), 300000) return { signature: sig.buffer } } catch (err) { + console.error(err) + this.#timeout.resume() throw new Error('Failed to sign message', { cause: err }) } } @@ -230,7 +237,7 @@ export class Vault { if (encrypted == null) { throw new TypeError('Wallet encrypted data is required') } - clearTimeout(this.#timeout) + this.#timeout?.pause() await this.#decryptWallet(type, key, iv, encrypted) if (!(this.#seed instanceof ArrayBuffer)) { throw new TypeError('Invalid seed') @@ -239,10 +246,11 @@ export class Vault { throw new TypeError('Invalid mnemonic') } this.#locked = false - this.#timeout = setTimeout(() => this.lock(), 300000) + this.#timeout = new Timer(() => this.lock(), 300000) return { isUnlocked: !this.#locked } } catch (err) { console.error(err) + this.#timeout?.resume() throw new Error('Failed to unlock wallet', { cause: err }) } } @@ -261,12 +269,13 @@ export class Vault { if (key == null || keySalt == null) { throw new TypeError('Wallet password is required') } - clearTimeout(this.#timeout) + this.#timeout.pause() const { iv, encrypted } = await this.#encryptWallet(key) - this.#timeout = setTimeout(() => this.lock(), 300000) + this.#timeout = new Timer(() => this.lock(), 300000) return { iv, salt: keySalt, encrypted } } catch (err) { console.error(err) + this.#timeout.resume() throw new Error('Failed to update wallet password', { cause: err }) } } @@ -310,6 +319,7 @@ export class Vault { } return { isVerified } } catch (err) { + console.error(err) throw new Error('Failed to export wallet', { cause: err }) } } @@ -591,5 +601,6 @@ export default ` const Bip44 = ${Bip44} const Blake2b = ${Blake2b} const NanoNaCl = ${NanoNaCl} + const Timer = ${Timer} const Vault = ${Vault} ` diff --git a/test/test.lock-unlock.mjs b/test/test.lock-unlock.mjs index 3370ecc..6ea0077 100644 --- a/test/test.lock-unlock.mjs +++ b/test/test.lock-unlock.mjs @@ -115,7 +115,7 @@ await Promise.all([ await assert.resolves(wallet.destroy()) }) - await test('fail to access a wallet after automatic lock', { skip: true }, async () => { + await test('wallet automatic lock resets after user activity', { skip: true }, async () => { const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) @@ -123,13 +123,34 @@ await Promise.all([ assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) await new Promise(async (resolve) => { - console.log('Waiting 5 minutes...') + console.log('Waiting 3 minutes...') setTimeout(async () => { + // should still be unlocked + const account = await wallet.account(0) + assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0) + resolve(null) + }, 180000) + }) + + await new Promise(async (resolve) => { + console.log('Timer should be reset, waiting 3 minutes...') + setTimeout(async () => { + // should still be unlocked from account() reset and not initial unlock + assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) + assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) + resolve(null) + }, 180000) + }) + + await new Promise(async (resolve) => { + console.log('Timer should not be reset by verify, waiting 3 minutes...') + setTimeout(async () => { + // should be locked from account() reset and not reset by verify() await assert.rejects(wallet.verify(NANO_TEST_VECTORS.MNEMONIC)) await assert.rejects(wallet.verify(NANO_TEST_VECTORS.BIP39_SEED)) await assert.resolves(wallet.destroy()) resolve(null) - }, 301000) + }, 180000) }) }) })