]> zoso.dev Git - libnemo.git/commitdiff
Update Safe to use browser session storage. Mock global sessionStorage in tests....
authorChris Duncan <chris@zoso.dev>
Sun, 13 Oct 2024 09:11:46 +0000 (02:11 -0700)
committerChris Duncan <chris@zoso.dev>
Sun, 13 Oct 2024 09:11:46 +0000 (02:11 -0700)
src/lib/safe.ts
src/lib/wallet.ts
test/TEST_VECTORS.js
test/create-wallet.test.mjs
test/derive-accounts.test.mjs
test/import-wallet.test.mjs
test/lock-unlock-wallet.mjs
test/refresh-accounts.test.mjs
test/tools.test.mjs

index 5da675f1e1854b16e17a0fd58367587a3ed4f1c9..7d9ae7babc3397777fe2992e2c122bee23b28dc5 100644 (file)
@@ -1,13 +1,13 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // 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<string, { encrypted: ArrayBuffer, iv: Entropy }> = 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<boolean>
   async put (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
-    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
index b139969cc5a007d18bfc4f7b70c8882efe93aa0d..539e9f966ea989f286a36ae5f311752f4034f1e9 100644 (file)
@@ -43,12 +43,14 @@ abstract class Wallet {
 \r
        abstract ckd (index: number): Promise<Account | null>\r
 \r
-       constructor (seed?: string, mnemonic?: Bip39Mnemonic) {\r
+       constructor (seed?: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
                if (this.constructor === Wallet) {\r
                        throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
                }\r
                this.#accounts = []\r
-               this.#id = new Entropy(16)\r
+               this.#id = id\r
+                       ? new Entropy(id)\r
+                       : new Entropy(16)\r
                this.#mnemonic = mnemonic ?? null\r
                this.#safe = new Safe()\r
                this.#seed = seed ?? null\r
@@ -153,21 +155,23 @@ abstract class Wallet {
        async lock (passkey: string | CryptoKey): Promise<boolean> {\r
                let success = true\r
                try {\r
-                       success &&= await this.#safe.overwrite(this.id, passkey as string, this.id)\r
-                       if (!success) {\r
-                               throw null\r
+                       const data: { id: string, mnemonic: string | null, seed: string | null } = {\r
+                               id: this.id,\r
+                               mnemonic: null,\r
+                               seed: null\r
                        }\r
                        if (this.#mnemonic instanceof Bip39Mnemonic) {\r
-                               success &&= await this.#safe.put('mnemonic', passkey as string, this.#mnemonic.phrase)\r
+                               data.mnemonic = this.#mnemonic.phrase\r
                        }\r
                        if (typeof this.#seed === 'string') {\r
-                               success &&= await this.#safe.put('seed', passkey as string, this.#seed)\r
+                               data.seed = this.#seed\r
                        }\r
+                       success &&= await this.#safe.put(this.id, passkey as string, data)\r
                        for (const a of this.#accounts) {\r
                                success &&= await a.lock(passkey as string)\r
                        }\r
                        if (!success) {\r
-                               throw success\r
+                               throw null\r
                        }\r
                } catch (err) {\r
                        throw new Error('Failed to lock wallet')\r
@@ -193,12 +197,10 @@ abstract class Wallet {
        async unlock (key: CryptoKey): Promise<boolean>\r
        async unlock (passkey: string | CryptoKey): Promise<boolean> {\r
                try {\r
-                       const id = await this.#safe.get(this.id, passkey as string)\r
+                       const { id, mnemonic, seed } = await this.#safe.get(this.id, passkey as string)\r
                        if (id !== this.id) {\r
                                throw null\r
                        }\r
-                       const mnemonic = await this.#safe.get('mnemonic', passkey as string)\r
-                       const seed = await this.#safe.get('seed', passkey as string)\r
                        for (const a of this.#accounts) {\r
                                await a.unlock(passkey as string)\r
                        }\r
@@ -240,11 +242,11 @@ abstract class Wallet {
 export class Bip44Wallet extends Wallet {\r
        static #isInternal: boolean = false\r
 \r
-       constructor (seed: string, mnemonic?: Bip39Mnemonic) {\r
+       constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
                if (!Bip44Wallet.#isInternal) {\r
                        throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
                }\r
-               super(seed, mnemonic)\r
+               super(seed, mnemonic, id)\r
                Bip44Wallet.#isInternal = false\r
        }\r
 \r
@@ -374,6 +376,11 @@ export class Bip44Wallet extends Wallet {
                return wallet\r
        }\r
 \r
+       static async import (id: string) {\r
+               Bip44Wallet.#isInternal = true\r
+               const wallet = new this('', undefined, id)\r
+       }\r
+\r
        /**\r
        * Derives BIP-44 Nano account private keys.\r
        *\r
@@ -405,11 +412,11 @@ export class Bip44Wallet extends Wallet {
 export class Blake2bWallet extends Wallet {\r
        static #isInternal: boolean = false\r
 \r
-       constructor (seed: string, mnemonic: Bip39Mnemonic) {\r
+       constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
                if (!Blake2bWallet.#isInternal) {\r
                        throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
                }\r
-               super(seed, mnemonic)\r
+               super(seed, mnemonic, id)\r
                Blake2bWallet.#isInternal = false\r
        }\r
 \r
@@ -501,6 +508,11 @@ export class Blake2bWallet extends Wallet {
                }\r
        }\r
 \r
+       static async import (id: string) {\r
+               Blake2bWallet.#isInternal = true\r
+               const wallet = new this('', undefined, id)\r
+       }\r
+\r
        /**\r
        * Derives BLAKE2b account private keys.\r
        *\r
index d0a458d741e69dfffe6331dd916ef85d3a61951a..f261a1c5886958faa6b4426056aa16962fae321d 100644 (file)
@@ -1,6 +1,20 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // 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'
index bbb18d6877349630583d5f2a722531c61422d12c..583e5dfe161197645e973050d8a3134e64772368 100644 (file)
@@ -5,8 +5,8 @@
 \r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
+import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 import { Account, Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js'\r
-import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 \r
 // WARNING: Do not send any funds to the test vectors below\r
 // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere\r
index 55697fe3fde52d73c8a3bc6ac0ab3f636cfa1b10..55cad424484c5f8bc9d1d246f34a06572662da9b 100644 (file)
@@ -5,8 +5,8 @@
 \r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
+import { NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js'\r
 import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js'\r
-import { NANO_TEST_VECTORS } from './TEST_VECTORS.js'\r
 \r
 // WARNING: Do not send any funds to the test vectors below\r
 // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere\r
index 032db9f3922f9a91448c8d8ec53f891dbc407735..1835f7c0422ba7bbf9174fec2a5f5b67c1672958 100644 (file)
@@ -5,8 +5,8 @@
 \r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
+import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 import { Account, Bip44Wallet, Blake2bWallet } from '../dist/main.js'\r
-import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 \r
 // WARNING: Do not send any funds to the test vectors below\r
 // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere\r
index b4a324def44a23b7eb27ff8a562d6b0df05f1fc1..d8c3dab24a53d31f680efea20b5c70e0bf6430a7 100644 (file)
@@ -5,8 +5,8 @@
 \r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
+import { NANO_TEST_VECTORS, STORAGE, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 import { Bip44Wallet, Blake2bWallet } from '../dist/main.js'\r
-import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
 \r
 const skip = false\r
 \r
index 9dfc8fd9bf293dde977869ab14844e808c0af217..d0ae268588bcd74c54af2a01c3aadfbbfa193858 100644 (file)
@@ -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
index 08de50d681fc8aeef9625ceae1b46db11e28b35e..ac9c252c06385c1a233b4e25e1d7be27fe498f33 100644 (file)
@@ -5,8 +5,8 @@
 \r
 import { describe, it } from 'node:test'\r
 import { strict as assert } from 'assert'\r
+import { RAW_MAX, NANO_TEST_VECTORS, STORAGE } from './TEST_VECTORS.js'\r
 import { Bip44Wallet, Account, SendBlock, Node, Tools } from '../dist/main.js'\r
-import { RAW_MAX, NANO_TEST_VECTORS } from './TEST_VECTORS.js'\r
 \r
 const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
 await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r