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
}
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()
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 })
}
}
}
return record
} catch (err) {
+ console.error(err)
throw new Error('Failed to load wallet', { cause: err })
} finally {
this.lock()
}
static lock (): NamedData<boolean> {
- clearTimeout(this.#timeout)
+ this.#timeout?.pause()
this.#mnemonic = undefined
this.#seed = undefined
this.#locked = true
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 })
}
}
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')
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 })
}
}
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 })
}
}
}
return { isVerified }
} catch (err) {
+ console.error(err)
throw new Error('Failed to export wallet', { cause: err })
}
}
const Bip44 = ${Bip44}
const Blake2b = ${Blake2b}
const NanoNaCl = ${NanoNaCl}
+ const Timer = ${Timer}
const Vault = ${Vault}
`
await assert.resolves(wallet.destroy())\r
})\r
\r
- await test('fail to access a wallet after automatic lock', { skip: true }, async () => {\r
+ await test('wallet automatic lock resets after user activity', { skip: true }, async () => {\r
const wallet = await Wallet.load('BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
\r
await new Promise(async (resolve) => {\r
- console.log('Waiting 5 minutes...')\r
+ console.log('Waiting 3 minutes...')\r
setTimeout(async () => {\r
+ // should still be unlocked\r
+ const account = await wallet.account(0)\r
+ assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_0)\r
+ resolve(null)\r
+ }, 180000)\r
+ })\r
+\r
+ await new Promise(async (resolve) => {\r
+ console.log('Timer should be reset, waiting 3 minutes...')\r
+ setTimeout(async () => {\r
+ // should still be unlocked from account() reset and not initial unlock\r
+ assert.ok(await wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
+ assert.ok(await wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
+ resolve(null)\r
+ }, 180000)\r
+ })\r
+\r
+ await new Promise(async (resolve) => {\r
+ console.log('Timer should not be reset by verify, waiting 3 minutes...')\r
+ setTimeout(async () => {\r
+ // should be locked from account() reset and not reset by verify()\r
await assert.rejects(wallet.verify(NANO_TEST_VECTORS.MNEMONIC))\r
await assert.rejects(wallet.verify(NANO_TEST_VECTORS.BIP39_SEED))\r
await assert.resolves(wallet.destroy())\r
resolve(null)\r
- }, 301000)\r
+ }, 180000)\r
})\r
})\r
})\r