From e9b0ffa6261ee54ef90afad832946f2a79e2fc39 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sun, 13 Oct 2024 02:11:46 -0700 Subject: [PATCH] Update Safe to use browser session storage. Mock global sessionStorage in tests. Update wallet to pack ID and both secrets in one JSON object when locking and unpack it all when unlocking. --- src/lib/safe.ts | 22 +++++++++++------- src/lib/wallet.ts | 42 ++++++++++++++++++++++------------ test/TEST_VECTORS.js | 14 ++++++++++++ test/create-wallet.test.mjs | 2 +- test/derive-accounts.test.mjs | 2 +- test/import-wallet.test.mjs | 2 +- test/lock-unlock-wallet.mjs | 2 +- test/refresh-accounts.test.mjs | 2 +- test/tools.test.mjs | 2 +- 9 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 5da675f..7d9ae7b 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import { buffer, utf8 } from './convert.js' +import { buffer, hex, utf8 } from './convert.js' import { Entropy } from './entropy.js' const { subtle } = globalThis.crypto +const storage = globalThis.sessionStorage const ERR_MSG = 'Failed to store item in Safe' export class Safe { - #items: Map = new Map() /** * Encrypts data with a password and stores it in the Safe. @@ -18,7 +18,7 @@ export class Safe { */ async put (name: string, key: CryptoKey, data: any): Promise async put (name: string, passkey: string | CryptoKey, data: any): Promise { - if (this.#items.get(name)) { + if (storage.getItem(name)) { throw new Error(ERR_MSG) } return this.overwrite(name, passkey as string, data) @@ -54,12 +54,16 @@ export class Safe { data = JSON.stringify(data) const encoded = utf8.toBytes(data) const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encoded) - this.#items.set(name, { encrypted, iv }) + const record = { + encrypted: buffer.toHex(encrypted), + iv: iv.hex + } + storage.setItem(name, JSON.stringify(record)) passkey = '' } catch (err) { throw new Error(ERR_MSG) } - return this.#items.has(name) + return (storage.getItem(name) != null) } /** @@ -75,11 +79,13 @@ export class Safe { return null } - const item = this.#items.get(name) + const item = storage.getItem(name) if (item == null) { return null } - const { encrypted, iv } = item + const record = JSON.parse(item) + const encrypted = hex.toBytes(record.encrypted) + const iv = new Entropy(record.iv) try { if (typeof passkey === 'string') { @@ -95,7 +101,7 @@ export class Safe { const decoded = buffer.toUtf8(decrypted) const data = JSON.parse(decoded) passkey = '' - this.#items.delete(name) + storage.removeItem(name) return data } catch (err) { return null diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index b139969..539e9f9 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -43,12 +43,14 @@ abstract class Wallet { abstract ckd (index: number): Promise - constructor (seed?: string, mnemonic?: Bip39Mnemonic) { + constructor (seed?: string, mnemonic?: Bip39Mnemonic, id?: string) { if (this.constructor === Wallet) { throw new Error('Wallet is an abstract class and cannot be instantiated directly.') } this.#accounts = [] - this.#id = new Entropy(16) + this.#id = id + ? new Entropy(id) + : new Entropy(16) this.#mnemonic = mnemonic ?? null this.#safe = new Safe() this.#seed = seed ?? null @@ -153,21 +155,23 @@ abstract class Wallet { async lock (passkey: string | CryptoKey): Promise { let success = true try { - success &&= await this.#safe.overwrite(this.id, passkey as string, this.id) - if (!success) { - throw null + const data: { id: string, mnemonic: string | null, seed: string | null } = { + id: this.id, + mnemonic: null, + seed: null } if (this.#mnemonic instanceof Bip39Mnemonic) { - success &&= await this.#safe.put('mnemonic', passkey as string, this.#mnemonic.phrase) + data.mnemonic = this.#mnemonic.phrase } if (typeof this.#seed === 'string') { - success &&= await this.#safe.put('seed', passkey as string, this.#seed) + data.seed = this.#seed } + success &&= await this.#safe.put(this.id, passkey as string, data) for (const a of this.#accounts) { success &&= await a.lock(passkey as string) } if (!success) { - throw success + throw null } } catch (err) { throw new Error('Failed to lock wallet') @@ -193,12 +197,10 @@ abstract class Wallet { async unlock (key: CryptoKey): Promise async unlock (passkey: string | CryptoKey): Promise { try { - const id = await this.#safe.get(this.id, passkey as string) + const { id, mnemonic, seed } = await this.#safe.get(this.id, passkey as string) if (id !== this.id) { throw null } - const mnemonic = await this.#safe.get('mnemonic', passkey as string) - const seed = await this.#safe.get('seed', passkey as string) for (const a of this.#accounts) { await a.unlock(passkey as string) } @@ -240,11 +242,11 @@ abstract class Wallet { export class Bip44Wallet extends Wallet { static #isInternal: boolean = false - constructor (seed: string, mnemonic?: Bip39Mnemonic) { + constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) { if (!Bip44Wallet.#isInternal) { throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) } - super(seed, mnemonic) + super(seed, mnemonic, id) Bip44Wallet.#isInternal = false } @@ -374,6 +376,11 @@ export class Bip44Wallet extends Wallet { return wallet } + static async import (id: string) { + Bip44Wallet.#isInternal = true + const wallet = new this('', undefined, id) + } + /** * Derives BIP-44 Nano account private keys. * @@ -405,11 +412,11 @@ export class Bip44Wallet extends Wallet { export class Blake2bWallet extends Wallet { static #isInternal: boolean = false - constructor (seed: string, mnemonic: Bip39Mnemonic) { + constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) { if (!Blake2bWallet.#isInternal) { throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) } - super(seed, mnemonic) + super(seed, mnemonic, id) Blake2bWallet.#isInternal = false } @@ -501,6 +508,11 @@ export class Blake2bWallet extends Wallet { } } + static async import (id: string) { + Blake2bWallet.#isInternal = true + const wallet = new this('', undefined, id) + } + /** * Derives BLAKE2b account private keys. * diff --git a/test/TEST_VECTORS.js b/test/TEST_VECTORS.js index d0a458d..f261a1c 100644 --- a/test/TEST_VECTORS.js +++ b/test/TEST_VECTORS.js @@ -1,6 +1,20 @@ // SPDX-FileCopyrightText: 2024 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later +export const STORAGE = (() => { + const _sessionStorage = {} + Object.defineProperty(globalThis, 'sessionStorage', { + value: { + length: Object.getOwnPropertyNames(_sessionStorage).length, + setItem: (key, value) => _sessionStorage[key] = value, + getItem: (key) => _sessionStorage[key], + removeItem: (key) => delete _sessionStorage[key] + }, + configurable: true, + enumerable: true + }) +})() + export const GENESIS_ADDRESS = 'nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3' export const RAW_MAX = '340282366920938463463374607431768211455' export const SUPPLY_MAX = '133248297920938463463374607431768211455' diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs index bbb18d6..583e5df 100644 --- a/test/create-wallet.test.mjs +++ b/test/create-wallet.test.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' import { Account, Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' -import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere diff --git a/test/derive-accounts.test.mjs b/test/derive-accounts.test.mjs index 55697fe..55cad42 100644 --- a/test/derive-accounts.test.mjs +++ b/test/derive-accounts.test.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' -import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere diff --git a/test/import-wallet.test.mjs b/test/import-wallet.test.mjs index 032db9f..1835f7c 100644 --- a/test/import-wallet.test.mjs +++ b/test/import-wallet.test.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' import { Account, Bip44Wallet, Blake2bWallet } from '../dist/main.js' -import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere diff --git a/test/lock-unlock-wallet.mjs b/test/lock-unlock-wallet.mjs index b4a324d..d8c3dab 100644 --- a/test/lock-unlock-wallet.mjs +++ b/test/lock-unlock-wallet.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' import { Bip44Wallet, Blake2bWallet } from '../dist/main.js' -import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' const skip = false diff --git a/test/refresh-accounts.test.mjs b/test/refresh-accounts.test.mjs index 9dfc8fd..d0ae268 100644 --- a/test/refresh-accounts.test.mjs +++ b/test/refresh-accounts.test.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' import { Account, Bip44Wallet, Node } from '../dist/main.js' -import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' // WARNING: Do not send any funds to the test vectors below // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere diff --git a/test/tools.test.mjs b/test/tools.test.mjs index 08de50d..ac9c252 100644 --- a/test/tools.test.mjs +++ b/test/tools.test.mjs @@ -5,8 +5,8 @@ import { describe, it } from 'node:test' import { strict as assert } from 'assert' +import { RAW_MAX, NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js' import { Bip44Wallet, Account, SendBlock, Node, Tools } from '../dist/main.js' -import { RAW_MAX, NANO_TEST_VECTORS } from './TEST_VECTORS.js' const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) -- 2.34.1