From 13bfd56cd76a1ca671ac8e5556d89690a336caa6 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 31 Jul 2025 12:05:45 -0700 Subject: [PATCH] Project compiling again, passkey is now the new safe and everything needs to be updated. --- package.json | 2 +- src/lib/account.ts | 47 +-- src/lib/block.ts | 2 +- src/lib/safe/bip44-ckd.ts | 32 -- src/lib/safe/blake2b-ckd.ts | 28 ++ src/lib/safe/index.ts | 2 +- src/lib/safe/passkey.ts | 341 ---------------- src/lib/safe/safe.ts | 645 +++++++++++++++++------------- src/lib/safe/worker-queue.ts | 2 - src/lib/wallets/bip44-wallet.ts | 10 +- src/lib/wallets/blake2b-wallet.ts | 4 +- src/lib/wallets/wallet.ts | 113 ++---- src/types.d.ts | 6 +- 13 files changed, 452 insertions(+), 782 deletions(-) create mode 100644 src/lib/safe/blake2b-ckd.ts delete mode 100644 src/lib/safe/passkey.ts diff --git a/package.json b/package.json index ddc841b..954ed43 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/lib/account.ts b/src/lib/account.ts index 3af15a9..2d75e92 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -197,15 +197,15 @@ export class Account { * @param {Key} password - Required to decrypt the private key for signing * @returns {Promise} Hexadecimal-formatted 64-byte signature */ - async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string): Promise { - try { - const signature = await NanoNaCl.detached(hex.toBytes(block.hash), new Uint8Array(await this.#getPrivateKey(password))) - block.signature = bytes.toHex(signature) - return block.signature - } catch (err) { - throw new Error(`Failed to sign block`, { cause: err }) - } - } + // async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string): Promise { + // try { + // const signature = await NanoNaCl.detached(hex.toBytes(block.hash), new Uint8Array(await this.#getPrivateKey(password))) + // block.signature = bytes.toHex(signature) + // return block.signature + // } catch (err) { + // throw new Error(`Failed to sign block`, { cause: err }) + // } + // } /** * Validates a Nano address with 'nano' and 'xrb' prefixes @@ -254,27 +254,6 @@ export class Account { return publicKey } - /** - * Retrieves and decrypts the private key of the Account. The same password - * used to lock it must be used to unlock it. - * - * @param {string} password Used previously to lock the Account - * @returns {Promise} Promise for buffer of private key - */ - async #getPrivateKey (password: string): Promise { - try { - const response = await SafeWorker.request({ - method: 'fetch', - names: this.publicKey, - store: 'Account', - password: utf8.toBuffer(password) - }) - return response[this.publicKey] - } catch (err) { - throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err }) - } - } - /** * Instantiates an Account object from its private key which is then encrypted * and stored in IndexedDB. The corresponding public key will automatically be @@ -315,10 +294,10 @@ export class Account { } try { - const { result } = await SafeWorker.request(privateAccounts) - if (!result) { - throw null - } + // const { result } = await SafeWorker.request(privateAccounts) + // if (!result) { + // throw null + // } return accounts } catch (err) { throw new Error(`Failed to lock Accounts`, { cause: err }) diff --git a/src/lib/block.ts b/src/lib/block.ts index 352e3e5..d0d880f 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -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 }) diff --git a/src/lib/safe/bip44-ckd.ts b/src/lib/safe/bip44-ckd.ts index ed412f5..489854e 100644 --- a/src/lib/safe/bip44-ckd.ts +++ b/src/lib/safe/bip44-ckd.ts @@ -1,8 +1,6 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { NamedData } from '#types' - type ExtendedKey = { privateKey: DataView chainCode: DataView @@ -14,36 +12,6 @@ export class Bip44Ckd { static HARDENED_OFFSET = 0x80000000 static SLIP10_ED25519 = 'ed25519 seed' - static async work (data: NamedData): Promise> { - 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 = {} - 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 index 0000000..8110317 --- /dev/null +++ b/src/lib/safe/blake2b-ckd.ts @@ -0,0 +1,28 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Blake2b } from '#src/lib/blake2b.js' +import { bytes, hex } from '#src/lib/convert.js' + +/** +* Derives account private keys from a wallet seed using the BLAKE2b hashing +* algorithm. +* +* Separately, account public keys are derived from the private key using the +* Ed25519 key algorithm, and account addresses are derived from the public key +* as described in the Nano documentation. +* https://docs.nano.org/integration-guides/the-basics/ +* +* @param {ArrayBuffer} seed - 32-byte secret seed of the wallet +* @param {number} index - Account to derive +* @returns {ArrayBuffer} Private key for the account +*/ +export class Blake2bCkd { + static ckd (seed: ArrayBuffer, index: number): ArrayBuffer { + const indexHex = Math.floor(index).toString(16).padStart(8, '0').toUpperCase() + const inputHex = `${bytes.toHex(new Uint8Array(seed))}${indexHex}`.padStart(72, '0') + const inputBytes = hex.toBytes(inputHex) + const privateKey = new Blake2b(32).update(inputBytes).digest() + return privateKey.buffer + } +} diff --git a/src/lib/safe/index.ts b/src/lib/safe/index.ts index de3b2eb..e944442 100644 --- a/src/lib/safe/index.ts +++ b/src/lib/safe/index.ts @@ -1,4 +1,4 @@ //! SPDX-FileCopyrightText: 2025 Chris Duncan //! 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 index 6bcb981..0000000 --- a/src/lib/safe/passkey.ts +++ /dev/null @@ -1,341 +0,0 @@ -//! SPDX-FileCopyrightText: 2025 Chris Duncan -//! 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 = crypto.getRandomValues(new Uint8Array(32)) - static #type?: 'BIP-44' | 'BLAKE2b' - static #seed?: Uint8Array - static #mnemonic?: Bip39Mnemonic - static #parentPort?: any - - static { - NODE: this.#parentPort = parentPort - const listener = async (message: MessageEvent): Promise => { - 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 { - 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> { - 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 { - 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 { - 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> { - 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 { - 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> { - try { - if (this.#locked) { - throw new Error('Wallet is locked') - } - if (this.#seed == null) { - throw new Error('Wallet seed not found') - } - const result: NamedData = {} - 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) { - 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 { - 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> { - 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> { - if (this.#seed == null) { - throw new Error('Wallet seed not found') - } - const data: NamedData = { - 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} -` diff --git a/src/lib/safe/safe.ts b/src/lib/safe/safe.ts index 96e6aee..3e1eea9 100644 --- a/src/lib/safe/safe.ts +++ b/src/lib/safe/safe.ts @@ -3,367 +3,458 @@ '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> { - 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): Promise => { + 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 { + static async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, mnemonicSalt?: string): Promise> { 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 { - 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> { + 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> { 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 { + 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> { - if (password == null || !(password instanceof ArrayBuffer)) { - throw new TypeError('Invalid password') - } - const results: NamedData = {} + static async sign (index?: number, data?: ArrayBuffer): Promise> { 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> { - 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> { + 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 = {} + /** + * 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> { 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 { - 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 { - 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> { + 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 { - 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 { - 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> { + if (this.#seed == null) { + throw new Error('Wallet seed not found') + } + const data: NamedData = { + 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 { - 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 { - 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} ` diff --git a/src/lib/safe/worker-queue.ts b/src/lib/safe/worker-queue.ts index 899fe40..1143167 100644 --- a/src/lib/safe/worker-queue.ts +++ b/src/lib/safe/worker-queue.ts @@ -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) diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts index 242777f..cc02eff 100644 --- a/src/lib/wallets/bip44-wallet.ts +++ b/src/lib/wallets/bip44-wallet.ts @@ -7,7 +7,7 @@ import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js' import { hex } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Key, KeyPair } from '#types' -import { PasskeyWorker } from '../safe' +import { SafeWorker } from '#workers' /** * Hierarchical deterministic (HD) wallet created by using a source of entropy to @@ -91,7 +91,7 @@ export class Bip44Wallet extends Wallet { throw new Error('Error importing Bip44Wallet from entropy', { cause: err }) } try { - await wallet.lock(password) + await wallet.lock() } catch (err) { await wallet.destroy() throw new Error('Error locking Bip44Wallet while importing from entropy', { cause: err }) @@ -119,7 +119,7 @@ export class Bip44Wallet extends Wallet { throw new Error('Error importing Bip44Wallet from mnemonic', { cause: err }) } try { - await wallet.lock(password) + await wallet.lock() } catch (err) { await wallet.destroy() throw new Error('Error locking Bip44Wallet while importing from mnemonic', { cause: err }) @@ -147,7 +147,7 @@ export class Bip44Wallet extends Wallet { Bip44Wallet.#isInternal = true const wallet = new this(id, hex.toBytes(seed)) try { - await wallet.lock(password) + await wallet.lock() } catch (err) { await wallet.destroy() throw new Error('Error locking Bip44Wallet while importing from seed', { cause: err }) @@ -180,7 +180,7 @@ export class Bip44Wallet extends Wallet { if (this.isLocked) { throw new Error('wallet must be unlocked to derive accounts') } - const results = await PasskeyWorker.request({ + const results = await SafeWorker.request({ action: 'derive', type: 'BIP-44', indexes, diff --git a/src/lib/wallets/blake2b-wallet.ts b/src/lib/wallets/blake2b-wallet.ts index f576d10..ed7fe13 100644 --- a/src/lib/wallets/blake2b-wallet.ts +++ b/src/lib/wallets/blake2b-wallet.ts @@ -83,7 +83,7 @@ export class Blake2bWallet extends Wallet { Blake2bWallet.#isInternal = true const wallet = new this(id, s, m) try { - await wallet.lock(password) + await wallet.lock() } catch (err) { await wallet.destroy() throw new Error('Error locking Blake2bWallet while importing from seed', { cause: err }) @@ -110,7 +110,7 @@ export class Blake2bWallet extends Wallet { throw new Error('Error importing Blake2bWallet from mnemonic', { cause: err }) } try { - await wallet.lock(password) + await wallet.lock() } catch (err) { await wallet.destroy() throw new Error('Error locking Blake2bWallet while importing from mnemonic', { cause: err }) diff --git a/src/lib/wallets/wallet.ts b/src/lib/wallets/wallet.ts index 1981193..725ccff 100644 --- a/src/lib/wallets/wallet.ts +++ b/src/lib/wallets/wallet.ts @@ -6,6 +6,7 @@ import { ChangeBlock, ReceiveBlock, SendBlock } from '#src/lib/block.js' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' import { ADDRESS_GAP } from '#src/lib/constants.js' import { bytes, hex, utf8 } from '#src/lib/convert.js' +import { Database } from '#src/lib/database.js' import { Entropy } from '#src/lib/entropy.js' import { Rpc } from '#src/lib/rpc.js' import { KeyPair, NamedData, WalletType } from '#types' @@ -21,37 +22,12 @@ import { SafeWorker } from '#workers' export abstract class Wallet { abstract ckd (index: number[]): Promise - /** - * Retrieves all wallet IDs from the Safe. - * - * @returns Array of hexadecimal-formatted wallet IDs - */ - static async export (): Promise | null> { - try { - const response = await SafeWorker.request({ - method: 'export', - store: 'Wallet' - }) - const ids = Object.keys(response) - const data: NamedData = {} - ids.map(i => { - const [type, id] = i.split('_') - if (data[type] == null) { - data[type] = [id] - } else { - data[type].push(id) - } - }) - return data - } catch (err) { - console.error(err) - return null - } - } + static #DB_NAME = 'Wallet' #accounts: AccountList #id: Entropy #locked: boolean + #lockTimer?: any #m?: Bip39Mnemonic #s?: Uint8Array #type: WalletType @@ -185,11 +161,7 @@ export abstract class Wallet { this.#accounts[a].destroy() delete this.#accounts[a] } - await SafeWorker.request({ - store: 'Wallet', - method: 'destroy', - names: this.id - }) + await Database.delete(this.id, Wallet.#DB_NAME) } catch (err) { console.error(err) throw new Error('failed to destroy wallet', { cause: err }) @@ -200,33 +172,14 @@ export abstract class Wallet { * Locks the wallet and all currently derived accounts with a password that * will be needed to unlock it later. * - * @param {string} password Used to lock the wallet * @returns True if successfully locked */ - async lock (password: string): Promise { + async lock (): Promise { try { - if (typeof password !== 'string') { - throw new TypeError('Invalid password') - } - const serialized = JSON.stringify({ - id: this.id, - mnemonic: this.#m?.phrase, - seed: this.#s == null ? this.#s : bytes.toHex(this.#s) + const { isLocked } = await SafeWorker.request({ + action: 'lock' }) - const success = await SafeWorker.request({ - method: 'store', - store: 'Wallet', - [this.id]: utf8.toBuffer(serialized), - password: utf8.toBuffer(password) - }) - if (!success) { - throw null - } - this.#m?.destroy() - bytes.erase(this.#s) - this.#m = undefined - this.#s = undefined - this.#locked = true + this.#locked = isLocked return this.#locked } catch (err) { throw new Error('failed to lock wallet', { cause: err }) @@ -273,8 +226,14 @@ export abstract class Wallet { if (this.#locked) throw new Error('Wallet must be unlocked to sign') if (this.#s == null) throw new Error('Wallet seed not found') try { - const account = await this.account(index) - return await account.sign(block, this.seed) + const { signature } = await SafeWorker.request({ + action: 'sign', + index, + data: JSON.stringify(block) + }) + const sig = bytes.toHex(new Uint8Array(signature)) + block.signature = sig + return sig } catch (err) { throw new Error(`Failed to sign block`, { cause: err }) } @@ -286,40 +245,28 @@ export abstract class Wallet { * @param {string} password Used previously to lock the wallet * @returns True if successfully unlocked */ - async unlock (password: string): Promise { - let response: NamedData + async unlock (password: string, iv: ArrayBuffer, salt: ArrayBuffer): Promise { try { if (typeof password !== 'string') { throw new TypeError('Invalid password') } - const response = await SafeWorker.request({ - method: 'fetch', - names: this.id, - store: 'Wallet', - password: utf8.toBuffer(password) + const unlockRequest = SafeWorker.request({ + action: 'unlock', + password: utf8.toBuffer(password), + iv, + salt }) - const decoded = bytes.toUtf8(new Uint8Array(response[this.id])) - const deserialized = JSON.parse(decoded) - let { id, mnemonic, seed } = deserialized - if (id == null) { - throw new Error('ID is null') - } - id = await Entropy.import(id.split('_')[1]) - if (id.hex !== this.#id.hex) { - throw new Error('ID does not match') - } - if (mnemonic != null) { - this.#m = await Bip39Mnemonic.fromPhrase(mnemonic) - mnemonic = null - } - if (seed != null) { - this.#s = hex.toBytes(seed) - seed = null + password = '' + const { isUnlocked } = await unlockRequest + if (isUnlocked) { + this.#lockTimer = setTimeout(this.lock, 120) + } else { + throw new Error('Request to wallet worker failed') } - this.#locked = false + this.#locked = isUnlocked return true } catch (err) { - throw new Error('failed to unlock wallet', { cause: err }) + throw new Error('Failed to unlock wallet', { cause: err }) } } diff --git a/src/types.d.ts b/src/types.d.ts index 5246dbe..28e4ad6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -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 (data: NamedData): Promise> terminate (): void } -export declare const Bip44CkdWorker: WorkerQueue -export declare const SafeWorker: WorkerQueue + +export declare const PasskeyWorker: WorkerQueue export type UnknownNumber = number | unknown -- 2.47.3