]> git.codecow.com Git - libnemo.git/commitdiff
Project compiling again, passkey is now the new safe and everything needs to be updated.
authorChris Duncan <chris@zoso.dev>
Thu, 31 Jul 2025 19:05:45 +0000 (12:05 -0700)
committerChris Duncan <chris@zoso.dev>
Thu, 31 Jul 2025 19:05:45 +0000 (12:05 -0700)
13 files changed:
package.json
src/lib/account.ts
src/lib/block.ts
src/lib/safe/bip44-ckd.ts
src/lib/safe/blake2b-ckd.ts [new file with mode: 0644]
src/lib/safe/index.ts
src/lib/safe/passkey.ts [deleted file]
src/lib/safe/safe.ts
src/lib/safe/worker-queue.ts
src/lib/wallets/bip44-wallet.ts
src/lib/wallets/blake2b-wallet.ts
src/lib/wallets/wallet.ts
src/types.d.ts

index ddc841b7f56dcb136337e5aa4fbbdfa81bdfc1e2..954ed43d8bc7148d278e8b8364721b8f94b8ccfa 100644 (file)
@@ -56,7 +56,7 @@
        "imports": {
                "#src/*": "./src/*",
                "#types": "./src/types.d.ts",
-               "#workers": "./src/lib/workers/index.js"
+               "#workers": "./src/lib/safe/index.js"
        },
        "dependencies": {
                "nano-pow": "^5.1.4"
index 3af15a988a78fdaca641e2a16d6a05f28a3f54c7..2d75e9227534d4f4da41b2fe526d30fa7e8b0e59 100644 (file)
@@ -197,15 +197,15 @@ export class Account {
        * @param {Key} password - Required to decrypt the private key for signing\r
        * @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
        */\r
-       async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string): Promise<string> {\r
-               try {\r
-                       const signature = await NanoNaCl.detached(hex.toBytes(block.hash), new Uint8Array(await this.#getPrivateKey(password)))\r
-                       block.signature = bytes.toHex(signature)\r
-                       return block.signature\r
-               } catch (err) {\r
-                       throw new Error(`Failed to sign block`, { cause: err })\r
-               }\r
-       }\r
+       // async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string): Promise<string> {\r
+       //      try {\r
+       //              const signature = await NanoNaCl.detached(hex.toBytes(block.hash), new Uint8Array(await this.#getPrivateKey(password)))\r
+       //              block.signature = bytes.toHex(signature)\r
+       //              return block.signature\r
+       //      } catch (err) {\r
+       //              throw new Error(`Failed to sign block`, { cause: err })\r
+       //      }\r
+       // }\r
 \r
        /**\r
        * Validates a Nano address with 'nano' and 'xrb' prefixes\r
@@ -254,27 +254,6 @@ export class Account {
                return publicKey\r
        }\r
 \r
-       /**\r
-       * Retrieves and decrypts the private key of the Account. The same password\r
-       * used to lock it must be used to unlock it.\r
-       *\r
-       * @param {string} password Used previously to lock the Account\r
-       * @returns {Promise<ArrayBuffer>} Promise for buffer of private key\r
-       */\r
-       async #getPrivateKey (password: string): Promise<ArrayBuffer> {\r
-               try {\r
-                       const response = await SafeWorker.request<ArrayBuffer>({\r
-                               method: 'fetch',\r
-                               names: this.publicKey,\r
-                               store: 'Account',\r
-                               password: utf8.toBuffer(password)\r
-                       })\r
-                       return response[this.publicKey]\r
-               } catch (err) {\r
-                       throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err })\r
-               }\r
-       }\r
-\r
        /**\r
        * Instantiates an Account object from its private key which is then encrypted\r
        * and stored in IndexedDB. The corresponding public key will automatically be\r
@@ -315,10 +294,10 @@ export class Account {
                }\r
 \r
                try {\r
-                       const { result } = await SafeWorker.request(privateAccounts)\r
-                       if (!result) {\r
-                               throw null\r
-                       }\r
+                       // const { result } = await SafeWorker.request(privateAccounts)\r
+                       // if (!result) {\r
+                       //      throw null\r
+                       // }\r
                        return accounts\r
                } catch (err) {\r
                        throw new Error(`Failed to lock Accounts`, { cause: err })\r
index 352e3e5963c7f689afda4ea70f6949a22c51c71a..d0d880f8c73cbba3bd3841ce507d79cd33ae9051 100644 (file)
@@ -165,7 +165,7 @@ abstract class Block {
                } else if (typeof input === 'string') {
                        try {
                                const account = await Account.import({ index: 0, privateKey: input }, '')
-                               this.signature = await account.sign(this, '')
+                               // this.signature = await account.sign(this, '')
                                await account.destroy()
                        } catch (err) {
                                throw new Error(`Failed to sign block`, { cause: err })
index ed412f5cffd3b704ab5c596b0359f30aaa8e4fc2..489854eb6f666a64d68b6b48f0321078c90480ec 100644 (file)
@@ -1,8 +1,6 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { NamedData } from '#types'
-
 type ExtendedKey = {
        privateKey: DataView<ArrayBuffer>
        chainCode: DataView
@@ -14,36 +12,6 @@ export class Bip44Ckd {
        static HARDENED_OFFSET = 0x80000000
        static SLIP10_ED25519 = 'ed25519 seed'
 
-       static async work (data: NamedData): Promise<NamedData<ArrayBuffer>> {
-               if (data.coin != null && (typeof data.coin !== 'number' || !Number.isInteger(data.coin))) {
-                       throw new TypeError('BIP-44 coin derivation level must be an integer')
-               }
-               if (!Array.isArray(data.indexes) || data.indexes.some(i => !Number.isInteger(i))) {
-                       throw new TypeError('BIP-44 account indexes must be an array of integers')
-               }
-               if (!(data.seed instanceof ArrayBuffer)) {
-                       throw new TypeError('BIP-44 seed must be an ArrayBuffer')
-               }
-               const coin: number = data.coin
-               const indexes = Array.isArray(data.indexes)
-                       ? data.indexes
-                       : [data.indexes]
-               const seed = data.seed
-               const privateKeys: NamedData<ArrayBuffer> = {}
-               for (const i of indexes) {
-                       if (typeof i !== 'number' || !Number.isInteger(i)) {
-                               throw new TypeError('BIP-44 account derivation level must be an integer')
-                       }
-                       try {
-                               const pk = await this.ckd(seed, coin, i)
-                               privateKeys[i] = pk
-                       } catch (err) {
-                               console.log('BIP-44 error')
-                       }
-               }
-               return privateKeys
-       }
-
        /**
        * Derives a private child key following the BIP-32 and BIP-44 derivation path
        * registered to the Nano block lattice. Only hardened child keys are defined.
diff --git a/src/lib/safe/blake2b-ckd.ts b/src/lib/safe/blake2b-ckd.ts
new file mode 100644 (file)
index 0000000..8110317
--- /dev/null
@@ -0,0 +1,28 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
+//! SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+import { Blake2b } from '#src/lib/blake2b.js'\r
+import { bytes, hex } from '#src/lib/convert.js'\r
+\r
+/**\r
+* Derives account private keys from a wallet seed using the BLAKE2b hashing\r
+* algorithm.\r
+*\r
+* Separately, account public keys are derived from the private key using the\r
+* Ed25519 key algorithm, and account addresses are derived from the public key\r
+* as described in the Nano documentation.\r
+* https://docs.nano.org/integration-guides/the-basics/\r
+*\r
+* @param {ArrayBuffer} seed - 32-byte secret seed of the wallet\r
+* @param {number} index - Account to derive\r
+* @returns {ArrayBuffer} Private key for the account\r
+*/\r
+export class Blake2bCkd {\r
+       static ckd (seed: ArrayBuffer, index: number): ArrayBuffer {\r
+               const indexHex = Math.floor(index).toString(16).padStart(8, '0').toUpperCase()\r
+               const inputHex = `${bytes.toHex(new Uint8Array(seed))}${indexHex}`.padStart(72, '0')\r
+               const inputBytes = hex.toBytes(inputHex)\r
+               const privateKey = new Blake2b(32).update(inputBytes).digest()\r
+               return privateKey.buffer\r
+       }\r
+}\r
index de3b2eb13e596e389b2f46e9d63b8d12967286e0..e944442f1cd0e07862a404df498a17ced19f3445 100644 (file)
@@ -1,4 +1,4 @@
 //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-export { PasskeyWorker, SafeWorker } from './worker-queue'
+export { SafeWorker } from './worker-queue'
diff --git a/src/lib/safe/passkey.ts b/src/lib/safe/passkey.ts
deleted file mode 100644 (file)
index 6bcb981..0000000
+++ /dev/null
@@ -1,341 +0,0 @@
-//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-//! SPDX-License-Identifier: GPL-3.0-or-later
-
-'use strict'
-
-import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'
-import { bytes, dec, hex, utf8 } from '#src/lib/convert.js'
-import { NamedData } from '#src/types.js'
-import { parentPort } from 'node:worker_threads'
-import { Bip39Words } from '../bip39-wordlist'
-import { NanoNaCl } from '../nano-nacl'
-import { Bip44Ckd } from './bip44-ckd'
-
-/**
-* Cross-platform worker for managing wallet secrets.
-*/
-export class Passkey {
-       static #locked: boolean = true
-       static #salt: Uint8Array<ArrayBuffer> = crypto.getRandomValues(new Uint8Array(32))
-       static #type?: 'BIP-44' | 'BLAKE2b'
-       static #seed?: Uint8Array<ArrayBuffer>
-       static #mnemonic?: Bip39Mnemonic
-       static #parentPort?: any
-
-       static {
-               NODE: this.#parentPort = parentPort
-               const listener = async (message: MessageEvent<any>): Promise<void> => {
-                       const { action, type, password, iv, salt, seed, mnemonic, index, encrypted, data } = this.#extractData(message.data)
-                       try {
-                               let result: NamedData
-                               switch (action) {
-                                       case 'STOP': {
-                                               BROWSER: close()
-                                               NODE: process.exit()
-                                       }
-                                       case 'create': {
-                                               result = await this.create(type, password)
-                                               break
-                                       }
-                                       case 'derive': {
-                                               result = await this.derive(type, index)
-                                               break
-                                       }
-                                       case 'import': {
-                                               result = await this.import(type, password, seed, mnemonic)
-                                               break
-                                       }
-                                       case 'lock': {
-                                               result = await this.lock()
-                                               break
-                                       }
-                                       case 'sign': {
-                                               result = await this.sign(index, data)
-                                               break
-                                       }
-                                       case 'unlock': {
-                                               result = await this.unlock(password, iv, salt, encrypted)
-                                               break
-                                       }
-                                       case 'verify': {
-                                               result = await this.verify(seed, mnemonic)
-                                               break
-                                       }
-                                       default: {
-                                               throw new Error(`Unknown wallet action '${action}'`)
-                                       }
-                               }
-                               const transfer = []
-                               for (const k of Object.keys(result)) {
-                                       if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) {
-                                               transfer.push(result[k])
-                                       }
-                               }
-                               //@ts-expect-error
-                               BROWSER: postMessage(result, transfer)
-                               //@ts-expect-error
-                               NODE: parentPort?.postMessage(result, transfer)
-                       } catch (err) {
-                               BROWSER: postMessage({ error: 'Failed to derive key from password', cause: err })
-                               NODE: parentPort?.postMessage({ error: 'Failed to derive key from password', cause: err })
-                       } finally {
-                               new Uint8Array(password).fill(0).buffer.transfer()
-                       }
-               }
-               BROWSER: addEventListener('message', listener)
-               NODE: this.#parentPort?.on('message', listener)
-       }
-
-       static async create (type?: 'BIP-44' | 'BLAKE2b', password?: ArrayBuffer, salt?: ArrayBuffer): Promise<boolean> {
-               try {
-                       const mnemonic = await Bip39Mnemonic.fromEntropy(crypto.getRandomValues(new Uint8Array(32)))
-                       return await this.import(type, password, undefined, mnemonic.phrase, salt)
-               } catch (err) {
-                       throw new Error('Failed to unlock wallet', { cause: err })
-               }
-       }
-
-       static async derive (type: 'BIP-44' | 'BLAKE2b', index: number): Promise<NamedData<ArrayBuffer>> {
-               try {
-                       if (this.#seed == null) {
-                               throw new Error('Wallet is locked')
-                       }
-                       const prv = await Bip44Ckd.nanoCKD(this.#seed.buffer, index)
-                       const pub = await NanoNaCl.convert(new Uint8Array(prv))
-                       return { publicKey: pub.buffer }
-               } catch (err) {
-                       throw new Error('Failed to derive account', { cause: err })
-               }
-       }
-
-       static async import (type?: 'BIP-44' | 'BLAKE2b', password?: ArrayBuffer, seed?: ArrayBuffer, mnemonic?: string, salt?: ArrayBuffer): Promise<boolean> {
-               try {
-                       if (type == null) {
-                               throw new TypeError('Wallet type is required')
-                       }
-                       if (password == null) {
-                               throw new TypeError('Wallet password is required')
-                       }
-                       if (seed == null && mnemonic == null) {
-                               throw new TypeError('Seed or mnemonic is required')
-                       }
-                       if (mnemonic == null && salt != null) {
-                               throw new TypeError('Mnemonic is required to use salt')
-                       }
-                       this.#type = type
-                       const key = await this.#createAesKey('decrypt', password, this.#salt.buffer)
-                       const encrypted = await this.#encryptWallet(key, this.#salt.buffer)
-                       if (!(encrypted.seed instanceof Uint8Array)) {
-                               throw new TypeError('Invalid seed')
-                       }
-                       if (encrypted.mnemonic != null && typeof encrypted.mnemonic !== 'string') {
-                               throw new TypeError('Invalid seed')
-                       }
-                       this.#seed = new Uint8Array(encrypted.seed)
-                       this.#mnemonic = await Bip39Mnemonic.fromPhrase(encrypted.mnemonic)
-                       this.#locked = false
-                       return this.#seed != null
-               } catch (err) {
-                       throw new Error('Failed to import wallet', { cause: err })
-               }
-       }
-
-       static async lock (): Promise<void> {
-               this.#mnemonic = undefined
-               this.#seed = undefined
-               this.#locked = true
-       }
-
-       /**
-       * Derives the account private key at a specified index, signs the input data,
-       * and returns a signature. The wallet must be unlocked prior to verification.
-       */
-       static async sign (index: number, data: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
-               try {
-                       if (this.#locked) {
-                               throw new Error('Wallet is locked')
-                       }
-                       if (this.#seed == null) {
-                               throw new Error('Wallet seed not found')
-                       }
-                       const prv = await Bip44Ckd.nanoCKD(this.#seed.buffer, index)
-                       const sig = await NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv))
-                       return { signature: sig.buffer }
-               } catch (err) {
-                       throw new Error('Failed to sign message', { cause: err })
-               }
-       }
-
-       /**
-       * Decrypts the input and sets the seed and, if it is included, the mnemonic.
-       */
-       static async unlock (encrypted: ArrayBuffer, password: ArrayBuffer, iv: ArrayBuffer, salt: ArrayBuffer): Promise<boolean> {
-               try {
-                       const key = await this.#createAesKey('decrypt', password, salt)
-                       const { seed, mnemonic } = await this.#decryptWallet(key, iv, encrypted)
-                       if (!(seed instanceof Uint8Array)) {
-                               throw new TypeError('Invalid seed')
-                       }
-                       if (mnemonic != null && typeof mnemonic !== 'string') {
-                               throw new TypeError('Invalid seed')
-                       }
-                       this.#seed = new Uint8Array(seed)
-                       this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic)
-                       this.#locked = false
-                       return this.#seed != null
-               } catch (err) {
-                       throw new Error('Failed to unlock wallet', { cause: err })
-               }
-       }
-
-       /**
-       * Checks the seed and, if it exists, the mnemonic against input. The wallet
-       * must be unlocked prior to verification.
-       */
-       static async verify (seed: ArrayBuffer, mnemonic: ArrayBuffer): Promise<NamedData<string>> {
-               try {
-                       if (this.#locked) {
-                               throw new Error('Wallet is locked')
-                       }
-                       if (this.#seed == null) {
-                               throw new Error('Wallet seed not found')
-                       }
-                       const result: NamedData<string> = {}
-                       if (this.#mnemonic != null) {
-                               result.mnemonic = this.#mnemonic
-                       }
-                       return {
-                               ...result,
-                               seed: bytes.toHex(this.#seed)
-                       }
-               } catch (err) {
-                       throw new Error('Failed to export wallet', { cause: err })
-               }
-       }
-
-       static async #bip39Mnemonic (entropy: Uint8Array<ArrayBuffer>) {
-               if (![16, 20, 24, 28, 32].includes(entropy.byteLength)) {
-                       throw new RangeError('Invalid entropy byte length for BIP-39')
-               }
-               const phraseLength = 0.75 * entropy.byteLength
-               const sha256sum = new Uint8Array(await crypto.subtle.digest('SHA-256', entropy))[0]
-               const checksumBitLength = entropy.byteLength / 4
-               const checksum = BigInt(sha256sum & ((1 << checksumBitLength) - 1))
-
-               let e = 0n
-               for (let i = 0; i < entropy.byteLength; i++) {
-                       e = e << 8n | BigInt(entropy[i])
-               }
-
-               let concatenation = (e << BigInt(checksumBitLength)) | checksum
-               const words: string[] = []
-               for (let i = 0; i < phraseLength; i++) {
-                       const wordBits = concatenation & 2047n
-                       const wordIndex = Number(wordBits)
-                       words.push(Bip39Words[wordIndex])
-                       concatenation >>= 11n
-               }
-               return words.join(' ').normalize('NFKD')
-       }
-
-       static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, salt: ArrayBuffer): Promise<CryptoKey> {
-               const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
-               const derivationAlgorithm: Pbkdf2Params = {
-                       name: 'PBKDF2',
-                       hash: 'SHA-512',
-                       iterations: 210000,
-                       salt
-               }
-               const derivedKeyType: AesKeyGenParams = {
-                       name: 'AES-GCM',
-                       length: 256
-               }
-               return await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
-       }
-
-       static async #decryptWallet (key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<NamedData<string | ArrayBuffer>> {
-               const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted)
-               const decoded = JSON.parse(bytes.toUtf8(new Uint8Array(decrypted)))
-               const seed = hex.toBuffer(decoded.seed)
-               const mnemonic = decoded.mnemonic
-               return { seed, mnemonic }
-       }
-
-       static async #encryptWallet (key: CryptoKey, salt: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
-               if (this.#seed == null) {
-                       throw new Error('Wallet seed not found')
-               }
-               const data: NamedData<string> = {
-                       seed: bytes.toHex(this.#seed)
-               }
-               if (this.#mnemonic?.phrase != null) data.mnemonic = this.#mnemonic.phrase
-               const iv = crypto.getRandomValues(new Uint8Array(32)).buffer
-               const encoded = utf8.toBytes(JSON.stringify(data))
-               const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)
-               return { iv, salt, encrypted }
-       }
-
-       static #extractData (data: unknown) {
-               if (data == null) {
-                       throw new TypeError('Worker received no data')
-               }
-               if (typeof data !== 'object') {
-                       throw new Error('Invalid data')
-               }
-               const dataObject = data as { [key: string]: unknown }
-               if (!('action' in dataObject)) {
-                       throw new TypeError('Wallet action is required')
-               }
-               if (dataObject.action !== 'STOP'
-                       && dataObject.action !== 'create'
-                       && dataObject.action !== 'derive'
-                       && dataObject.action !== 'import'
-                       && dataObject.action !== 'lock'
-                       && dataObject.action !== 'sign'
-                       && dataObject.action !== 'unlock'
-                       && dataObject.action !== 'verify') {
-                       throw new TypeError('Invalid wallet action')
-               }
-               const action = dataObject.action
-
-               if (dataObject.type !== undefined && dataObject.type !== 'BIP-44' && dataObject.type !== 'BLAKE2b') {
-                       throw new TypeError('Invalid wallet type', { cause: dataObject.type })
-               }
-               const type: 'BIP-44' | 'BLAKE2b' | undefined = dataObject.type
-
-               if (!('password' in dataObject) || !(dataObject.password instanceof ArrayBuffer)) {
-                       throw new TypeError('Password must be ArrayBuffer')
-               }
-               const password: ArrayBuffer = dataObject.password
-
-               if (action === 'unlock' && !(dataObject.iv instanceof ArrayBuffer)) {
-                       throw new TypeError('Initialization vector required to unlock wallet')
-               }
-               const iv: ArrayBuffer = action === 'unlock' && dataObject.iv instanceof ArrayBuffer
-                       ? dataObject.iv
-                       : crypto.getRandomValues(new Uint8Array(32)).buffer
-
-               if (action === 'unlock' && !(dataObject.salt instanceof ArrayBuffer)) {
-                       throw new TypeError('Salt required to unlock wallet')
-               }
-               const salt: ArrayBuffer = action === 'unlock' && dataObject.salt instanceof ArrayBuffer
-                       ? dataObject.salt
-                       : crypto.getRandomValues(new Uint8Array(32)).buffer
-
-               if (action === 'import' && !(dataObject.seed instanceof ArrayBuffer)) {
-                       throw new TypeError('Seed required to import wallet')
-               }
-               const seed: ArrayBuffer = action === 'import' && dataObject.seed instanceof ArrayBuffer
-                       ? dataObject.seed
-                       : crypto.getRandomValues(new Uint8Array(32)).buffer
-
-               return { action, type, password, iv, seed, mnemonic, salt, encrypted, indexes, data }
-       }
-}
-
-let importWorkerThreads = ''
-NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'`
-export default `
-       ${importWorkerThreads}
-       const Passkey = ${Passkey}
-`
index 96e6aee18c025cb664d6a7e8f9dae9985e8de978..3e1eea9216dbcd66c7219808ee74cc115eb96672 100644 (file)
 
 'use strict'
 
+import { parentPort } from 'node:worker_threads'
+import { Bip39Words } from '../bip39-wordlist'
 import { Bip44Ckd } from './bip44-ckd'
-import { Blake2b } from './blake2b'
-import { WorkerInterface } from './worker-interface'
-import { PBKDF2_ITERATIONS } from '#src/lib/constants.js'
-import { default as Convert, bytes } from '#src/lib/convert.js'
-import { Entropy } from '#src/lib/entropy.js'
-import { NamedData, SafeRecord } from '#types'
+import { Blake2bCkd } from './blake2b-ckd'
+import { NanoNaCl } from '../nano-nacl'
+import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'
+import { bytes, dec, hex, utf8 } from '#src/lib/convert.js'
+import { NamedData } from '#src/types.js'
 
 /**
-* Encrypts and stores data in the browser using IndexedDB.
+* Cross-platform worker for managing wallet secrets.
 */
-export class Safe extends WorkerInterface {
-       static DB_NAME = 'libnemo'
-       static DB_STORES = ['Wallet', 'Account', 'Rolodex']
-       static ERR_MSG = 'Failed to store item in Safe'
-       static #storage: IDBDatabase
-
+export class Safe {
+       static #locked: boolean = true
+       static #type?: 'BIP-44' | 'BLAKE2b'
+       static #seed?: ArrayBuffer
+       static #mnemonic?: Bip39Mnemonic
+       static #parentPort?: any
        static {
-               this.listen()
+               NODE: this.#parentPort = parentPort
        }
 
-       static async work (data: NamedData | unknown): Promise<NamedData<boolean | ArrayBuffer>> {
-               if (data == null) {
-                       throw new TypeError('Worker received no data')
-               }
-               if (typeof data !== 'object') {
-                       throw new TypeError('Invalid data')
-               }
-               let { method, names, store, password, ...buffers } = data as { [key: string]: unknown }
-               if (typeof method !== 'string') {
-                       throw new TypeError('Invalid method')
-               }
-               if (typeof names === 'string') names = [names]
-               function validateNames (names: unknown): asserts names is string[] {
-                       if (names !== undefined && (!Array.isArray(names) || names.some(n => typeof n !== 'string'))) {
-                               throw new TypeError('Invalid name')
-                       }
-               }
-               validateNames(names)
-               if (typeof store !== 'string') {
-                       throw new TypeError('Invalid store')
-               }
-               if (password != null && !(password instanceof ArrayBuffer)) {
-                       throw new TypeError('Invalid password')
-               }
-               this.#storage = await this.#open(this.DB_NAME)
-               try {
-                       switch (method) {
-                               case 'store': {
-                                       return { result: await this.store(buffers, store, password) }
-                               }
-                               case 'fetch': {
-                                       return await this.fetch(names, store, password)
-                               }
-                               case 'export': {
-                                       return await this.export(store, password)
-                               }
-                               case 'destroy': {
-                                       return { result: await this.destroy(names, store) }
+       static {
+               NODE: this.#parentPort = parentPort
+               const listener = async (message: MessageEvent<any>): Promise<void> => {
+                       const {
+                               action,
+                               type,
+                               key,
+                               keySalt,
+                               seed,
+                               mnemonicPhrase,
+                               mnemonicSalt,
+                               index,
+                               encrypted,
+                               data
+                       } = await this.#extractData(message.data)
+                       try {
+                               let result: NamedData
+                               switch (action) {
+                                       case 'STOP': {
+                                               BROWSER: close()
+                                               NODE: process.exit()
+                                       }
+                                       case 'create': {
+                                               result = await this.create(type, key)
+                                               break
+                                       }
+                                       case 'derive': {
+                                               result = await this.derive(index)
+                                               break
+                                       }
+                                       case 'import': {
+                                               result = await this.import(type, key, mnemonicPhrase ?? seed, mnemonicSalt)
+                                               break
+                                       }
+                                       case 'lock': {
+                                               result = await this.lock()
+                                               break
+                                       }
+                                       case 'sign': {
+                                               result = await this.sign(index, data)
+                                               break
+                                       }
+                                       case 'unlock': {
+                                               result = await this.unlock(key, keySalt, encrypted)
+                                               break
+                                       }
+                                       case 'verify': {
+                                               result = await this.verify(seed, mnemonicPhrase)
+                                               break
+                                       }
+                                       default: {
+                                               throw new Error(`Unknown wallet action '${action}'`)
+                                       }
                                }
-                               default: {
-                                       throw new Error(`unknown Safe method ${method}`)
+                               const transfer = []
+                               for (const k of Object.keys(result)) {
+                                       if (result[k] instanceof ArrayBuffer || result[k] instanceof CryptoKey) {
+                                               transfer.push(result[k])
+                                       }
                                }
+                               //@ts-expect-error
+                               BROWSER: postMessage(result, transfer)
+                               //@ts-expect-error
+                               NODE: parentPort?.postMessage(result, transfer)
+                       } catch (err) {
+                               BROWSER: postMessage({ error: 'Failed to derive key from password', cause: err })
+                               NODE: parentPort?.postMessage({ error: 'Failed to derive key from password', cause: err })
                        }
-               } catch (err) {
-                       console.error(err)
-                       throw new Error('Safe error', { cause: err })
                }
+               BROWSER: addEventListener('message', listener)
+               NODE: this.#parentPort?.on('message', listener)
        }
 
        /**
-       * Removes data from the Safe without decrypting.
+       * Generates a new mnemonic and seed and then returns the initialization vector
+       * vector, salt, and encrypted data representing the wallet in a locked state.
        */
-       static async destroy (names: string[], store: string): Promise<boolean> {
+       static async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
-                       return await this.#delete(names, store)
+                       const entropy = crypto.getRandomValues(new Uint8Array(32))
+                       const mnemonicPhrase = (await Bip39Mnemonic.fromEntropy(entropy)).phrase
+                       return await this.import(type, key, mnemonicPhrase, mnemonicSalt)
                } catch (err) {
-                       console.error(err)
-                       throw new Error(this.ERR_MSG)
+                       throw new Error('Failed to unlock wallet', { cause: err })
                }
        }
 
        /**
-       * Encrypts data with a password byte array and stores it in the Safe.
+       * Derives the private and public keys of a child account from the current
+       * wallet seed at a specified index and then returns the public key. The wallet
+       * must be unlocked prior to derivation.
        */
-       static async store (data: NamedData | unknown, store: string | unknown, password: ArrayBuffer | unknown): Promise<boolean> {
-               this.#isDataValid(data)
-               if (typeof store !== 'string' || store === '') {
-                       throw new Error('Invalid database store name')
-               }
-               if (!(password instanceof ArrayBuffer)) {
-                       throw new Error('Invalid password')
+       static async derive (index?: number): Promise<NamedData<ArrayBuffer>> {
+               try {
+                       if (this.#locked) {
+                               throw new Error('Wallet is locked')
+                       }
+                       if (this.#seed == null) {
+                               throw new Error('Wallet seed not found')
+                       }
+                       if (this.#type !== 'BIP-44' && this.#type !== 'BLAKE2b') {
+                               throw new Error('Invalid wallet type')
+                       }
+                       if (typeof index !== 'number') {
+                               throw new Error('Invalid wallet account index')
+                       }
+                       const prv = this.#type === 'BIP-44'
+                               ? await Bip44Ckd.nanoCKD(this.#seed, index)
+                               : await Blake2bCkd.ckd(this.#seed, index)
+                       const pub = await NanoNaCl.convert(new Uint8Array(prv))
+                       return { publicKey: pub.buffer }
+               } catch (err) {
+                       throw new Error('Failed to derive account', { cause: err })
                }
+       }
 
-               const records: SafeRecord[] = []
+       /**
+       * Encrypts an existing seed or mnemonic+salt and returns the initialization
+       * vector, salt, and encrypted data representing the wallet in a locked state.
+       */
+       static async import (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, secret?: string | ArrayBuffer, salt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
-                       const salt = await Entropy.create()
-                       const encryptionKey = await this.#createAesKey('encrypt', password, salt.buffer)
-                       for (const label of Object.keys(data)) {
-                               const iv = await Entropy.create()
-                               const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, data[label])
-                               const record: SafeRecord = {
-                                       iv: iv.buffer,
-                                       salt: salt.buffer,
-                                       label,
-                                       encrypted
+                       if (!this.#locked) {
+                               throw new Error('Wallet is in use')
+                       }
+                       if (type == null) {
+                               throw new TypeError('Wallet type is required')
+                       }
+                       if (type !== 'BIP-44' && type !== 'BLAKE2b') {
+                               throw new TypeError('Invalid wallet type')
+                       }
+                       if (key == null) {
+                               throw new TypeError('Wallet password is required')
+                       }
+                       if (secret == null) {
+                               throw new TypeError('Seed or mnemonic is required')
+                       }
+                       if (typeof secret !== 'string' && salt !== undefined) {
+                               throw new TypeError('Mnemonic must be a string')
+                       }
+                       if (type === 'BIP-44') {
+                               if (secret instanceof ArrayBuffer && (secret.byteLength < 16 || secret.byteLength < 32)) {
+                                       throw new RangeError('Seed for BIP-44 wallet must be 16-32 bytes')
+                               }
+                       }
+                       if (type === 'BLAKE2b') {
+                               if (secret instanceof ArrayBuffer && secret.byteLength !== 32) {
+                                       throw new RangeError('Invalid seed for BLAKE2b wallet')
                                }
-                               records.push(record)
                        }
-                       return await this.#put(records, store)
+                       this.#type = type
+                       if (secret instanceof ArrayBuffer) {
+                               this.#seed = secret
+                       } else {
+                               this.#mnemonic = await Bip39Mnemonic.fromPhrase(secret)
+                               this.#seed = type === 'BIP-44'
+                                       ? (await this.#mnemonic.toBip39Seed(salt ?? '')).buffer
+                                       : (await this.#mnemonic.toBlake2bSeed()).buffer
+                       }
+                       return await this.#encryptWallet(key)
                } catch (err) {
-                       throw new Error(this.ERR_MSG)
-               } finally {
-                       bytes.erase(password)
+                       this.lock()
+                       throw new Error('Failed to import wallet', { cause: err })
                }
        }
 
+       static lock (): NamedData<boolean> {
+               this.#mnemonic = undefined
+               this.#seed = undefined
+               this.#locked = true
+               return { isLocked: this.#locked }
+       }
+
        /**
-       * Retrieves data from the Safe and decrypts it with a password byte array.
+       * Derives the account private key at a specified index, signs the input data,
+       * and returns a signature. The wallet must be unlocked prior to verification.
        */
-       static async fetch (names: string[], store: string, password: ArrayBuffer | unknown): Promise<NamedData<ArrayBuffer>> {
-               if (password == null || !(password instanceof ArrayBuffer)) {
-                       throw new TypeError('Invalid password')
-               }
-               const results: NamedData<ArrayBuffer> = {}
+       static async sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
                try {
-                       const records: SafeRecord[] = await this.#get(names, store)
-                       if (records == null) {
-                               throw new Error('')
+                       if (this.#locked) {
+                               throw new Error('Wallet is locked')
+                       }
+                       if (this.#seed == null) {
+                               throw new Error('Wallet seed not found')
                        }
-                       const decryptionKeys: { [salt: string]: CryptoKey } = {}
-                       for (const record of records) {
-                               const salt = await Entropy.import(record.salt)
-                               decryptionKeys[salt.hex] ??= await this.#createAesKey('decrypt', password, salt.buffer)
-                               const iv = await Entropy.import(record.iv)
-                               const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKeys[salt.hex], record.encrypted)
-                               results[record.label] = decrypted
+                       if (index == null) {
+                               throw new Error('Wallet account index is required to sign')
                        }
-                       return results
+                       if (data == null) {
+                               throw new Error('Data to sign not found')
+                       }
+                       const prv = await Bip44Ckd.nanoCKD(this.#seed, index)
+                       const sig = await NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv))
+                       return { signature: sig.buffer }
                } catch (err) {
-                       console.error(err)
-                       throw new Error('Failed to get records', { cause: err })
-               } finally {
-                       bytes.erase(password)
+                       throw new Error('Failed to sign message', { cause: err })
                }
        }
 
        /**
-       * Retrieves all data from a specified Safe table. If a password is not
-       * provided, the records are returned as encrypted data.
+       * Decrypts the input and sets the seed and, if it is included, the mnemonic.
        */
-       static async export (store: string | unknown, password?: ArrayBuffer | unknown): Promise<NamedData<ArrayBuffer>> {
-               if (typeof store !== 'string' || store === '') {
-                       throw new Error('Invalid database store name')
-               }
-               if (password != null && !(password instanceof ArrayBuffer)) {
-                       throw new Error('Invalid password')
+       static async unlock (key: CryptoKey, iv: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
+               try {
+                       if (encrypted == null) {
+                               throw new TypeError('Wallet encrypted secrets required to unlock')
+                       }
+                       const { seed, mnemonic } = await this.#decryptWallet(key, iv, encrypted)
+                       if (!(seed instanceof ArrayBuffer)) {
+                               throw new TypeError('Invalid seed')
+                       }
+                       if (mnemonic != null && typeof mnemonic !== 'string') {
+                               throw new TypeError('Invalid seed')
+                       }
+                       this.#seed = seed
+                       this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic)
+                       this.#locked = false
+                       return { isUnlocked: !this.#locked }
+               } catch (err) {
+                       throw new Error('Failed to unlock wallet', { cause: err })
                }
+       }
 
-               const results: NamedData<ArrayBuffer> = {}
+       /**
+       * Checks the seed and, if it exists, the mnemonic against input. The wallet
+       * must be unlocked prior to verification.
+       */
+       static async verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Promise<NamedData<boolean>> {
                try {
-                       const records: SafeRecord[] = await this.#getAll(store)
-                       if (records == null) {
-                               throw new Error('')
+                       if (this.#locked) {
+                               throw new Error('Wallet is locked')
+                       }
+                       if (this.#seed == null) {
+                               throw new Error('Wallet seed not found')
+                       }
+                       if (seed == null && mnemonicPhrase == null) {
+                               throw new Error('Seed or mnemonic phrase not found')
+                       }
+                       if (seed != null && mnemonicPhrase != null) {
+                               throw new Error('Seed or mnemonic phrase must be verified separately')
                        }
-                       if (password instanceof ArrayBuffer) {
-                               const decryptionKeys: { [salt: string]: CryptoKey } = {}
-                               for (const record of records) {
-                                       const salt = await Entropy.import(record.salt)
-                                       decryptionKeys[salt.hex] ??= await this.#createAesKey('decrypt', password, salt.buffer)
-                                       const iv = await Entropy.import(record.iv)
-                                       const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKeys[salt.hex], record.encrypted)
-                                       results[record.label] = decrypted
+                       let isVerified = false
+                       if (seed != null) {
+                               if (seed.byteLength === this.#seed.byteLength) {
+                                       const userSeed = new Uint8Array(seed)
+                                       const thisSeed = new Uint8Array(this.#seed)
+                                       for (let i = 0; i < seed.byteLength; i++) {
+                                               if (userSeed[i] === thisSeed[i]) {
+                                                       isVerified = true
+                                               } else {
+                                                       isVerified = false
+                                                       break
+                                               }
+                                       }
                                }
-                       } else {
-                               for (const record of records) {
-                                       results[record.label] = record.encrypted
+                       }
+                       if (mnemonicPhrase != null) {
+                               if (mnemonicPhrase === this.#mnemonic?.phrase) {
+                                       isVerified = true
                                }
                        }
-                       return results
+                       return { isVerified }
                } catch (err) {
-                       console.error(err)
-                       throw new Error(`Failed to export ${store} records`, { cause: err })
-               } finally {
-                       if (password instanceof ArrayBuffer) bytes.erase(password)
+                       throw new Error('Failed to export wallet', { cause: err })
                }
        }
 
        static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, salt: ArrayBuffer): Promise<CryptoKey> {
-               const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+               const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+               new Uint8Array(password).fill(0).buffer.transfer()
                const derivationAlgorithm: Pbkdf2Params = {
                        name: 'PBKDF2',
                        hash: 'SHA-512',
-                       iterations: PBKDF2_ITERATIONS,
+                       iterations: 210000,
                        salt
                }
                const derivedKeyType: AesKeyGenParams = {
                        name: 'AES-GCM',
                        length: 256
                }
-               return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
+               return await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
        }
 
-       static async #delete (names: string[], store: string): Promise<boolean> {
-               const transaction = this.#storage.transaction(store, 'readwrite')
-               const db = transaction.objectStore(store)
-               return new Promise((resolve, reject) => {
-                       const requests = names.map(name => {
-                               const request = db.delete(name)
-                               request.onsuccess = (event) => {
-                               }
-                               request.onerror = (event) => {
-                                       console.error('getAll request error before transaction committed')
-                               }
-                               return request
-                       })
-                       transaction.oncomplete = (event) => {
-                               for (const request of requests) {
-                                       if (request?.error != null) {
-                                               reject(request.error)
-                                       }
-                                       if (request.result !== undefined) {
-                                               resolve(false)
-                                       }
-                               }
-                               resolve(true)
-                       }
-                       transaction.onerror = (event) => {
-                               console.error('Database error')
-                               reject((event.target as IDBRequest).error)
-                       }
-               })
+       static async #decryptWallet (key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<NamedData<string | ArrayBuffer>> {
+               const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted)
+               const decoded = JSON.parse(bytes.toUtf8(new Uint8Array(decrypted)))
+               const seed = hex.toBuffer(decoded.seed)
+               const mnemonic = decoded.mnemonic
+               return { seed, mnemonic }
        }
 
-       static async #get (names: string[], store: string): Promise<SafeRecord[]> {
-               const transaction = this.#storage.transaction(store, 'readonly')
-               const db = transaction.objectStore(store)
-               return new Promise((resolve, reject) => {
-                       const requests = names.map(name => {
-                               const request = db.get(name)
-                               request.onsuccess = (event) => {
-                               }
-                               request.onerror = (event) => {
-                                       console.error('get request error before transaction committed')
-                               }
-                               return request
-                       })
-                       transaction.oncomplete = (event) => {
-                               const results = []
-                               for (const request of requests) {
-                                       if (request?.error == null && request.result != null) {
-                                               results.push(request.result)
-                                       }
-                               }
-                               resolve(results)
-                       }
-                       transaction.onerror = (event) => {
-                               console.error('Database error')
-                               reject((event.target as IDBRequest).error)
-                       }
-               })
-       }
-
-       static async #getAll (store: string): Promise<SafeRecord[]> {
-               const transaction = this.#storage.transaction(store, 'readonly')
-               const db = transaction.objectStore(store)
-               return new Promise((resolve, reject) => {
-                       const request = db.getAll()
-                       request.onsuccess = (event) => {
-                       }
-                       request.onerror = (event) => {
-                               console.error('getAll request error before transaction committed')
-                       }
-                       transaction.oncomplete = (event) => {
-                               if (request?.error != null) {
-                                       reject(request.error)
-                               } else if (request.result == null) {
-                                       reject('getAll request failed')
-                               } else {
-                                       resolve(request.result)
-                               }
-                       }
-                       transaction.onerror = (event) => {
-                               console.error('Database error')
-                               reject((event.target as IDBRequest).error)
-                       }
-               })
+       static async #encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
+               if (this.#seed == null) {
+                       throw new Error('Wallet seed not found')
+               }
+               const data: NamedData<string> = {
+                       seed: bytes.toHex(new Uint8Array(this.#seed))
+               }
+               if (this.#mnemonic?.phrase != null) data.mnemonic = this.#mnemonic.phrase
+               const iv = crypto.getRandomValues(new Uint8Array(32)).buffer
+               const encoded = utf8.toBytes(JSON.stringify(data))
+               const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)
+               return { iv, encrypted }
        }
 
-       static #isDataValid (data: unknown): asserts data is { [key: string]: ArrayBuffer } {
-               if (typeof data !== 'object') {
-                       throw new Error('Invalid data')
+       /**
+       * Parse inbound message from main thread into typechecked variables.
+       */
+       static async #extractData (message: unknown) {
+               // Message itself
+               if (message == null) {
+                       throw new TypeError('Worker received no data')
                }
-               const dataObject = data as { [key: string]: unknown }
-               if (Object.keys(dataObject).some(k => !(dataObject[k] instanceof ArrayBuffer))) {
+               if (typeof message !== 'object') {
                        throw new Error('Invalid data')
                }
-       }
+               const messageData = message as { [key: string]: unknown }
 
-       static async #open (database: string): Promise<IDBDatabase> {
-               return new Promise((resolve, reject) => {
-                       const request = indexedDB.open(database, 1)
-                       request.onupgradeneeded = (event) => {
-                               const db = (event.target as IDBOpenDBRequest).result
-                               for (const DB_STORE of this.DB_STORES) {
-                                       if (!db.objectStoreNames.contains(DB_STORE)) {
-                                               db.createObjectStore(DB_STORE)
-                                       }
-                               }
-                       }
-                       request.onsuccess = (event) => {
-                               resolve((event.target as IDBOpenDBRequest).result)
+               // Action for selecting method execution
+               if (!('action' in messageData)) {
+                       throw new TypeError('Wallet action is required')
+               }
+               if (messageData.action !== 'STOP'
+                       && messageData.action !== 'create'
+                       && messageData.action !== 'derive'
+                       && messageData.action !== 'import'
+                       && messageData.action !== 'lock'
+                       && messageData.action !== 'sign'
+                       && messageData.action !== 'unlock'
+                       && messageData.action !== 'verify') {
+                       throw new TypeError('Invalid wallet action')
+               }
+               const action = messageData.action
+
+               // Password for lock/unlock key
+               if ('password' in messageData || !(messageData.password instanceof ArrayBuffer)) {
+                       throw new TypeError('Password must be ArrayBuffer')
+               }
+               const password: ArrayBuffer = messageData.password
+
+               // IV for crypto key, included if unlocking or generated if creating
+               if (action === 'unlock' && !(messageData.iv instanceof ArrayBuffer)) {
+                       throw new TypeError('Initialization vector required to unlock wallet')
+               }
+               const iv: ArrayBuffer = action === 'unlock' && messageData.iv instanceof ArrayBuffer
+                       ? messageData.iv
+                       : crypto.getRandomValues(new Uint8Array(32)).buffer
+
+               // Salt for decryption key to unlock
+               if (action === 'unlock' && !(messageData.keySalt instanceof ArrayBuffer)) {
+                       throw new TypeError('Salt required to unlock wallet')
+               }
+               const keySalt: ArrayBuffer = action === 'unlock' && messageData.keySalt instanceof ArrayBuffer
+                       ? messageData.keySalt
+                       : crypto.getRandomValues(new Uint8Array(32)).buffer
+
+               // CryptoKey from password, decryption key if unlocking else encryption key
+               const key = await this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', password, keySalt)
+
+               // Type of wallet
+               if (messageData.type !== undefined && messageData.type !== 'BIP-44' && messageData.type !== 'BLAKE2b') {
+                       throw new TypeError('Invalid wallet type', { cause: messageData.type })
+               }
+               const type: 'BIP-44' | 'BLAKE2b' | undefined = messageData.type
+
+               // Seed to import
+               if (action === 'import' && !(messageData.seed instanceof ArrayBuffer)) {
+                       throw new TypeError('Seed required to import wallet')
+               }
+               const seed = messageData.seed instanceof ArrayBuffer
+                       ? messageData.seed
+                       : undefined
+
+               // Mnemonic phrase to import
+               if (action === 'import' && typeof messageData.mnemonicPhrase !== 'string') {
+                       throw new TypeError('Invalid mnemonic phrase')
+               }
+               const mnemonicPhrase = typeof messageData.mnemonicPhrase === 'string'
+                       ? messageData.mnemonicPhrase
+                       : undefined
+
+               // Mnemonic salt for mnemonic phrase to import
+               if (action === 'import' && messageData.mnemonicSalt != undefined && typeof messageData.mnemonicSalt !== 'string') {
+                       throw new TypeError('Invalid mnemonic salt for mnemonic phrase')
+               }
+               const mnemonicSalt = typeof messageData.mnemonicSalt === 'string'
+                       ? messageData.mnemonicSalt
+                       : undefined
+
+               // Encrypted seed and possibly mnemonic
+               if (action === 'unlock') {
+                       if (messageData.encrypted == null) {
+                               throw new TypeError('Wallet encrypted secrets not found')
                        }
-                       request.onerror = (event) => {
-                               reject(new Error('Database error', { cause: event }))
+                       if (!(messageData.encrypted instanceof ArrayBuffer)) {
+                               throw new TypeError('Invalid wallet encrypted secrets')
                        }
-               })
-       }
+               }
+               const encrypted = messageData.encrypted instanceof ArrayBuffer
+                       ? messageData.encrypted
+                       : undefined
 
-       static async #put (records: SafeRecord[], store: string): Promise<boolean> {
-               const transaction = this.#storage.transaction(store, 'readwrite')
-               const db = transaction.objectStore(store)
-               return new Promise((resolve, reject) => {
-                       const requests = records.map(record => {
-                               const request = db.put(record, record.label)
-                               request.onsuccess = (event) => {
-                               }
-                               request.onerror = (event) => {
-                                       console.error('put request error before transaction committed')
-                               }
-                               return request
-                       })
-                       transaction.oncomplete = (event) => {
-                               const results = []
-                               for (const request of requests) {
-                                       if (request?.error == null && request.result != null) {
-                                               results.push(request.result)
-                                       }
-                               }
-                               resolve(results.length > 0)
+               // Index for child account to derive or sign
+               if ((action === 'derive' || action === 'sign') && typeof messageData.index !== 'number') {
+                       throw new TypeError('Index is required to derive an account private key')
+               }
+               const index = typeof messageData.index === 'number'
+                       ? messageData.index
+                       : undefined
+
+               // Data to sign
+               if (action === 'sign') {
+                       if (messageData.data == null) {
+                               throw new TypeError('Data to sign not found')
                        }
-                       transaction.onerror = (event) => {
-                               console.error('Database error')
-                               reject((event.target as IDBRequest).error)
+                       if (!(messageData.data instanceof ArrayBuffer)) {
+                               throw new TypeError('Invalid data to sign')
                        }
-               })
+               }
+               const data = messageData.data instanceof ArrayBuffer
+                       ? messageData.data
+                       : undefined
+
+               return { action, type, key, iv, keySalt, seed, mnemonicPhrase, mnemonicSalt, encrypted, index, data }
        }
 }
 
 let importWorkerThreads = ''
-let importFakeIndexedDb = ''
 NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'`
-NODE: importFakeIndexedDb = `import 'fake-indexeddb/auto'`
 export default `
        ${importWorkerThreads}
-       ${importFakeIndexedDb}
-       ${Convert}
-       const PBKDF2_ITERATIONS = ${PBKDF2_ITERATIONS}
-       const Entropy = ${Entropy}
-       const Blake2b = ${Blake2b}
-       const Bip44Ckd = ${Bip44Ckd}
-       const WorkerInterface = ${WorkerInterface}
        const Safe = ${Safe}
 `
index 899fe4035abba7c41915bf082ff09cbd580ada2f..1143167131ff2ed72f62622a2ed59e634fe6fc73 100644 (file)
@@ -2,7 +2,6 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { Worker as NodeWorker } from 'node:worker_threads'
-import { default as passkey } from './passkey'
 import { default as safe } from './safe'
 import { Data, NamedData } from '#types'
 
@@ -112,5 +111,4 @@ export class WorkerQueue {
        }
 }
 
-export const PasskeyWorker = new WorkerQueue(passkey)
 export const SafeWorker = new WorkerQueue(safe)
index 242777f717795d1648a23bfb5d6f72b8b95fbfdf..cc02eff333681914bb9cda3f06eba8a611dbf6c1 100644 (file)
@@ -7,7 +7,7 @@ import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'
 import { hex } from '#src/lib/convert.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 import { Key, KeyPair } from '#types'\r
-import { PasskeyWorker } from '../safe'\r
+import { SafeWorker } from '#workers'\r
 \r
 /**\r
 * Hierarchical deterministic (HD) wallet created by using a source of entropy to\r
@@ -91,7 +91,7 @@ export class Bip44Wallet extends Wallet {
                        throw new Error('Error importing Bip44Wallet from entropy', { cause: err })\r
                }\r
                try {\r
-                       await wallet.lock(password)\r
+                       await wallet.lock()\r
                } catch (err) {\r
                        await wallet.destroy()\r
                        throw new Error('Error locking Bip44Wallet while importing from entropy', { cause: err })\r
@@ -119,7 +119,7 @@ export class Bip44Wallet extends Wallet {
                        throw new Error('Error importing Bip44Wallet from mnemonic', { cause: err })\r
                }\r
                try {\r
-                       await wallet.lock(password)\r
+                       await wallet.lock()\r
                } catch (err) {\r
                        await wallet.destroy()\r
                        throw new Error('Error locking Bip44Wallet while importing from mnemonic', { cause: err })\r
@@ -147,7 +147,7 @@ export class Bip44Wallet extends Wallet {
                Bip44Wallet.#isInternal = true\r
                const wallet = new this(id, hex.toBytes(seed))\r
                try {\r
-                       await wallet.lock(password)\r
+                       await wallet.lock()\r
                } catch (err) {\r
                        await wallet.destroy()\r
                        throw new Error('Error locking Bip44Wallet while importing from seed', { cause: err })\r
@@ -180,7 +180,7 @@ export class Bip44Wallet extends Wallet {
                if (this.isLocked) {\r
                        throw new Error('wallet must be unlocked to derive accounts')\r
                }\r
-               const results = await PasskeyWorker.request({\r
+               const results = await SafeWorker.request({\r
                        action: 'derive',\r
                        type: 'BIP-44',\r
                        indexes,\r
index f576d10006152a12b517dcb248720a53b29bc861..ed7fe133281e4eb6189ff2f7d204893d64db6498 100644 (file)
@@ -83,7 +83,7 @@ export class Blake2bWallet extends Wallet {
                Blake2bWallet.#isInternal = true\r
                const wallet = new this(id, s, m)\r
                try {\r
-                       await wallet.lock(password)\r
+                       await wallet.lock()\r
                } catch (err) {\r
                        await wallet.destroy()\r
                        throw new Error('Error locking Blake2bWallet while importing from seed', { cause: err })\r
@@ -110,7 +110,7 @@ export class Blake2bWallet extends Wallet {
                        throw new Error('Error importing Blake2bWallet from mnemonic', { cause: err })\r
                }\r
                try {\r
-                       await wallet.lock(password)\r
+                       await wallet.lock()\r
                } catch (err) {\r
                        await wallet.destroy()\r
                        throw new Error('Error locking Blake2bWallet while importing from mnemonic', { cause: err })\r
index 19811938af7c4b25ce83a4dd3de2d36fa6317698..725ccff4780645984c393d49e95d19997661d4c2 100644 (file)
@@ -6,6 +6,7 @@ import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js'
 import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
 import { ADDRESS_GAP } from '#src/lib/constants.js'\r
 import { bytes, hex, utf8 } from '#src/lib/convert.js'\r
+import { Database } from '#src/lib/database.js'\r
 import { Entropy } from '#src/lib/entropy.js'\r
 import { Rpc } from '#src/lib/rpc.js'\r
 import { KeyPair, NamedData, WalletType } from '#types'\r
@@ -21,37 +22,12 @@ import { SafeWorker } from '#workers'
 export abstract class Wallet {\r
        abstract ckd (index: number[]): Promise<KeyPair[]>\r
 \r
-       /**\r
-       * Retrieves all wallet IDs from the Safe.\r
-       *\r
-       * @returns Array of hexadecimal-formatted wallet IDs\r
-       */\r
-       static async export (): Promise<NamedData<string[]> | null> {\r
-               try {\r
-                       const response = await SafeWorker.request<ArrayBuffer>({\r
-                               method: 'export',\r
-                               store: 'Wallet'\r
-                       })\r
-                       const ids = Object.keys(response)\r
-                       const data: NamedData<string[]> = {}\r
-                       ids.map(i => {\r
-                               const [type, id] = i.split('_')\r
-                               if (data[type] == null) {\r
-                                       data[type] = [id]\r
-                               } else {\r
-                                       data[type].push(id)\r
-                               }\r
-                       })\r
-                       return data\r
-               } catch (err) {\r
-                       console.error(err)\r
-                       return null\r
-               }\r
-       }\r
+       static #DB_NAME = 'Wallet'\r
 \r
        #accounts: AccountList\r
        #id: Entropy\r
        #locked: boolean\r
+       #lockTimer?: any\r
        #m?: Bip39Mnemonic\r
        #s?: Uint8Array<ArrayBuffer>\r
        #type: WalletType\r
@@ -185,11 +161,7 @@ export abstract class Wallet {
                                this.#accounts[a].destroy()\r
                                delete this.#accounts[a]\r
                        }\r
-                       await SafeWorker.request<boolean>({\r
-                               store: 'Wallet',\r
-                               method: 'destroy',\r
-                               names: this.id\r
-                       })\r
+                       await Database.delete(this.id, Wallet.#DB_NAME)\r
                } catch (err) {\r
                        console.error(err)\r
                        throw new Error('failed to destroy wallet', { cause: err })\r
@@ -200,33 +172,14 @@ export abstract class Wallet {
        * Locks the wallet and all currently derived accounts with a password that\r
        * will be needed to unlock it later.\r
        *\r
-       * @param {string} password Used to lock the wallet\r
        * @returns True if successfully locked\r
        */\r
-       async lock (password: string): Promise<boolean> {\r
+       async lock (): Promise<boolean> {\r
                try {\r
-                       if (typeof password !== 'string') {\r
-                               throw new TypeError('Invalid password')\r
-                       }\r
-                       const serialized = JSON.stringify({\r
-                               id: this.id,\r
-                               mnemonic: this.#m?.phrase,\r
-                               seed: this.#s == null ? this.#s : bytes.toHex(this.#s)\r
+                       const { isLocked } = await SafeWorker.request<boolean>({\r
+                               action: 'lock'\r
                        })\r
-                       const success = await SafeWorker.request({\r
-                               method: 'store',\r
-                               store: 'Wallet',\r
-                               [this.id]: utf8.toBuffer(serialized),\r
-                               password: utf8.toBuffer(password)\r
-                       })\r
-                       if (!success) {\r
-                               throw null\r
-                       }\r
-                       this.#m?.destroy()\r
-                       bytes.erase(this.#s)\r
-                       this.#m = undefined\r
-                       this.#s = undefined\r
-                       this.#locked = true\r
+                       this.#locked = isLocked\r
                        return this.#locked\r
                } catch (err) {\r
                        throw new Error('failed to lock wallet', { cause: err })\r
@@ -273,8 +226,14 @@ export abstract class Wallet {
                if (this.#locked) throw new Error('Wallet must be unlocked to sign')\r
                if (this.#s == null) throw new Error('Wallet seed not found')\r
                try {\r
-                       const account = await this.account(index)\r
-                       return await account.sign(block, this.seed)\r
+                       const { signature } = await SafeWorker.request<ArrayBuffer>({\r
+                               action: 'sign',\r
+                               index,\r
+                               data: JSON.stringify(block)\r
+                       })\r
+                       const sig = bytes.toHex(new Uint8Array(signature))\r
+                       block.signature = sig\r
+                       return sig\r
                } catch (err) {\r
                        throw new Error(`Failed to sign block`, { cause: err })\r
                }\r
@@ -286,40 +245,28 @@ export abstract class Wallet {
        * @param {string} password Used previously to lock the wallet\r
        * @returns True if successfully unlocked\r
        */\r
-       async unlock (password: string): Promise<boolean> {\r
-               let response: NamedData<ArrayBuffer>\r
+       async unlock (password: string, iv: ArrayBuffer, salt: ArrayBuffer): Promise<boolean> {\r
                try {\r
                        if (typeof password !== 'string') {\r
                                throw new TypeError('Invalid password')\r
                        }\r
-                       const response = await SafeWorker.request<ArrayBuffer>({\r
-                               method: 'fetch',\r
-                               names: this.id,\r
-                               store: 'Wallet',\r
-                               password: utf8.toBuffer(password)\r
+                       const unlockRequest = SafeWorker.request<boolean>({\r
+                               action: 'unlock',\r
+                               password: utf8.toBuffer(password),\r
+                               iv,\r
+                               salt\r
                        })\r
-                       const decoded = bytes.toUtf8(new Uint8Array(response[this.id]))\r
-                       const deserialized = JSON.parse(decoded)\r
-                       let { id, mnemonic, seed } = deserialized\r
-                       if (id == null) {\r
-                               throw new Error('ID is null')\r
-                       }\r
-                       id = await Entropy.import(id.split('_')[1])\r
-                       if (id.hex !== this.#id.hex) {\r
-                               throw new Error('ID does not match')\r
-                       }\r
-                       if (mnemonic != null) {\r
-                               this.#m = await Bip39Mnemonic.fromPhrase(mnemonic)\r
-                               mnemonic = null\r
-                       }\r
-                       if (seed != null) {\r
-                               this.#s = hex.toBytes(seed)\r
-                               seed = null\r
+                       password = ''\r
+                       const { isUnlocked } = await unlockRequest\r
+                       if (isUnlocked) {\r
+                               this.#lockTimer = setTimeout(this.lock, 120)\r
+                       } else {\r
+                               throw new Error('Request to wallet worker failed')\r
                        }\r
-                       this.#locked = false\r
+                       this.#locked = isUnlocked\r
                        return true\r
                } catch (err) {\r
-                       throw new Error('failed to unlock wallet', { cause: err })\r
+                       throw new Error('Failed to unlock wallet', { cause: err })\r
                }\r
        }\r
 \r
index 5246dbe2f1bf6c491a91094afae9b21814c0368c..28e4ad64511438632c3ba9c815c180e15f9b792e 100644 (file)
@@ -377,7 +377,7 @@ export declare class ChangeBlock extends Block {
        constructor (account: Account | string, balance: string, representative: Account | string, frontier: string, work?: string)
 }
 
-export type Data = boolean | number[] | string | string[] | ArrayBuffer | CryptoKey
+export type Data = boolean | number | number[] | string | string[] | ArrayBuffer | CryptoKey
 
 /**
 * Represents a cryptographically strong source of entropy suitable for use in
@@ -1053,8 +1053,8 @@ export declare class WorkerQueue {
        prioritize<T extends Data> (data: NamedData): Promise<NamedData<T>>
        terminate (): void
 }
-export declare const Bip44CkdWorker: WorkerQueue
-export declare const SafeWorker: WorkerQueue
+
+export declare const PasskeyWorker: WorkerQueue
 
 export type UnknownNumber = number | unknown