From a3493b87963f2e2d178e8ed7ff4189b56b39e067 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 30 Jul 2025 21:03:26 -0700 Subject: [PATCH] Continue developing secure wallet worker. --- src/lib/account.ts | 11 -- src/lib/bip39-mnemonic.ts | 12 +- src/lib/{db.ts => database.ts} | 38 +++-- src/lib/rolodex.ts | 78 +++------ src/lib/safe/passkey.ts | 275 +++++++++++++++++++++----------- src/lib/wallets/bip44-wallet.ts | 5 +- 6 files changed, 246 insertions(+), 173 deletions(-) rename src/lib/{db.ts => database.ts} (75%) diff --git a/src/lib/account.ts b/src/lib/account.ts index 5fea41c..3af15a9 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -8,7 +8,6 @@ import { base32, bytes, hex, utf8 } from './convert' import { NanoNaCl } from './nano-nacl' import { Rpc } from './rpc' import { Key, KeyPair, NamedData } from '#types' -import { SafeWorker } from '#workers' /** * Represents a single Nano address and the associated public key. To include the @@ -76,16 +75,6 @@ export class Account { this.#receivable = undefined this.#representative = undefined this.#weight = undefined - try { - await SafeWorker.request({ - method: 'destroy', - store: 'Account', - names: this.publicKey - }) - } catch (err) { - console.error(err) - throw new Error('failed to destroy account', { cause: err }) - } } /** diff --git a/src/lib/bip39-mnemonic.ts b/src/lib/bip39-mnemonic.ts index a8dab41..2acdccf 100644 --- a/src/lib/bip39-mnemonic.ts +++ b/src/lib/bip39-mnemonic.ts @@ -173,14 +173,14 @@ export class Bip39Mnemonic { * @returns {Promise} Promise for seed as hexadecimal string */ async toBip39Seed (passphrase: string, format: 'hex'): Promise - async toBip39Seed (passphrase: string, format?: 'hex'): Promise> { + async toBip39Seed (passphrase: unknown, format?: 'hex'): Promise> { if (this.phrase == null) { throw new Error('BIP-39 mnemonic phrase not found') } if (this.#bip39Seed == null) { - if (passphrase == null || typeof passphrase !== 'string') { - passphrase = '' - } + const salt = (passphrase == null || typeof passphrase !== 'string') + ? '' + : passphrase const keyData = utf8.toBytes(this.phrase) const phraseKey = await globalThis.crypto.subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveBits', 'deriveKey']) const derivedKeyType: HmacImportParams = { @@ -189,11 +189,11 @@ export class Bip39Mnemonic { length: 512 } - passphrase = `mnemonic${passphrase.normalize('NFKD')}` + passphrase = `mnemonic${salt.normalize('NFKD')}` const algorithm: Pbkdf2Params = { name: 'PBKDF2', hash: 'SHA-512', - salt: utf8.toBytes(passphrase), + salt: utf8.toBytes(salt), iterations: BIP39_ITERATIONS } const seedKey = await globalThis.crypto.subtle.deriveKey(algorithm, phraseKey, derivedKeyType, true, ['sign']) diff --git a/src/lib/db.ts b/src/lib/database.ts similarity index 75% rename from src/lib/db.ts rename to src/lib/database.ts index ac7a887..f6b3578 100644 --- a/src/lib/db.ts +++ b/src/lib/database.ts @@ -3,16 +3,24 @@ 'use strict' -import { NamedData } from '#types' +import { Data, NamedData } from '#types' /** * Encrypts and stores data in the browser using IndexedDB. */ -export class Safe { +export class Database { static DB_NAME = 'libnemo' static DB_STORES = ['Wallet', 'Account', 'Rolodex'] static #storage: IDBDatabase + /** + * Deletes a record from a datastore. + * + * @param {string} name - Index key of the record to delete + * @param {string} store - Datastore from which to delete the record + * @returns {Promise} True if data was successfully removed, else false + */ + static async delete (name: string, store: string): Promise /** * Deletes records from a datastore. * @@ -20,7 +28,9 @@ export class Safe { * @param {string} store - Datastore from which to delete records * @returns {Promise} True if data was successfully removed, else false */ - static async delete (names: string[], store: string): Promise { + static async delete (names: string[], store: string): Promise + static async delete (names: string | string[], store: string): Promise { + if (!Array.isArray(names)) names = [names] this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readwrite') const db = transaction.objectStore(store) @@ -43,6 +53,14 @@ export class Safe { }) } + /** + * Gets a specific record from a datastore. + * + * @param {string[]} names - Index key of the record to get + * @param {string} store - Datastore from which to get the record + * @returns {Promise} Object of key-value pairs + */ + static async get (name: string, store: string): Promise> /** * Gets specific records from a datastore. * @@ -50,14 +68,16 @@ export class Safe { * @param {string} store - Datastore from which to get records * @returns {Promise} Object of key-value pairs */ - static async get (names: string[], store: string): Promise { + static async get (names: string[], store: string): Promise> + static async get (names: string | string[], store: string): Promise> { + if (!Array.isArray(names)) names = [names] this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readonly') const db = transaction.objectStore(store) return new Promise((resolve, reject) => { const requests = names.map(name => db.get(name)) transaction.oncomplete = (event) => { - const results: NamedData = {} + const results: NamedData = {} for (const request of requests) { results[request.result.id] = request.error ?? request.result } @@ -75,7 +95,7 @@ export class Safe { * @param {string} store - Datastore from which to get records * @returns {Promise} Object of key-value pairs */ - static async getAll (store: string): Promise { + static async getAll (store: string): Promise> { this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readonly') const db = transaction.objectStore(store) @@ -87,9 +107,9 @@ export class Safe { } else if (request.result == null) { reject('getAll request failed') } else { - const results: NamedData = {} + const results: NamedData = {} for (const result of request.result) { - results[result.id] = request.error ?? request.result + results[result.id] = request.error ?? result[result.id] } resolve(results) } @@ -107,7 +127,7 @@ export class Safe { * @param {string} store - Datastore in which to put records * @returns {Promise<(IDBValidKey | DOMException)[]>} Index keys of the records inserted */ - static async put (data: NamedData, store: string): Promise<(IDBValidKey | DOMException)[]> { + static async put (data: NamedData, store: string): Promise<(IDBValidKey | DOMException)[]> { this.#storage ??= await this.#open(this.DB_NAME) const transaction = this.#storage.transaction(store, 'readwrite') const db = transaction.objectStore(store) diff --git a/src/lib/rolodex.ts b/src/lib/rolodex.ts index 83b77f2..06a434d 100644 --- a/src/lib/rolodex.ts +++ b/src/lib/rolodex.ts @@ -3,15 +3,16 @@ import { Account } from './account' import { bytes, utf8 } from './convert' +import { Database } from './database' import { verify } from './tools' import { NamedData } from '#types' -import { SafeWorker } from '#workers' /** * Represents a basic address book of Nano accounts. Multiple addresses can be * saved under one nickname. */ export class Rolodex { + static #DB_NAME = 'Rolodex' /** * Adds an address to the rolodex under a specific nickname. * @@ -52,22 +53,22 @@ export class Rolodex { return true } const data: NamedData = { - method: 'store', - store: 'Rolodex', - password: utf8.toBuffer(''), [address]: utf8.toBuffer(name) } if (existingName != null) { - const filteredAddresses = (await this.getAddresses(existingName)).filter(a => a !== address).sort() - data[existingName] = utf8.toBuffer(JSON.stringify(filteredAddresses)) + const existingAddresses = await this.getAddresses(existingName) + data[existingName] = existingAddresses.filter(a => a !== address).sort() } const existingAddresses = await this.getAddresses(name) existingAddresses.push(account.address) - data[name] = utf8.toBuffer(JSON.stringify(existingAddresses)) - const { result } = await SafeWorker.request(data) - return result + data[name] = existingAddresses + const results = await Database.put(data, this.#DB_NAME) + if (results.length !== Object.keys(data).length) { + throw new Error('Unexpected results from adding address', { cause: results }) + } + return true } catch (err) { - throw new Error('failed to add address', { cause: err }) + throw new Error('Failed to add address', { cause: err }) } } @@ -83,21 +84,15 @@ export class Rolodex { return false } const addresses = (await this.getAddresses(name)).filter(a => a !== address).sort() - const { result: isUpdated } = await SafeWorker.request({ - method: 'store', - store: 'Rolodex', - password: utf8.toBuffer(''), - [name]: utf8.toBuffer(JSON.stringify(addresses)) - }) + const data = { + [name]: addresses + } + const isUpdated = await Database.put(data, this.#DB_NAME) if (!isUpdated) { throw new Error('failed to remove address from existing name') } - const { result } = await SafeWorker.request({ - method: 'destroy', - store: 'Rolodex', - names: address - }) - return result + const isDeleted = await Database.delete(address, this.#DB_NAME) + return isDeleted } /** @@ -107,18 +102,9 @@ export class Rolodex { * @returns {Promise} Promise for true if name and related addresses successfully removed, else false */ static async deleteName (name: string): Promise { - const data: NamedData = { - method: 'destroy', - store: 'Rolodex' - } - const names: string[] = [name] - const addresses = await this.getAddresses(name) - for (const address of addresses) { - names.push(address) - } - data.names = names - const { result } = await SafeWorker.request(data) - return result + const data = await this.getAddresses(name) + data.push(name) + return await Database.delete(data, this.#DB_NAME) } /** @@ -129,15 +115,10 @@ export class Rolodex { */ static async getAddresses (name: string): Promise { try { - const response = await SafeWorker.request({ - method: 'fetch', - names: name, - store: 'Rolodex', - password: utf8.toBuffer('') - }) + const response = await Database.get(name, this.#DB_NAME) const addresses = response[name] return addresses - ? JSON.parse(bytes.toUtf8(new Uint8Array(addresses))).sort() + ? addresses.sort() : [] } catch (err) { console.error(err) @@ -152,11 +133,7 @@ export class Rolodex { */ static async getAllNames (): Promise { try { - const response = await SafeWorker.request({ - method: 'export', - store: 'Rolodex', - password: utf8.toBuffer('') - }) + const response = await Database.getAll(this.#DB_NAME) return Object.keys(response).filter(v => v.slice(0, 5) !== 'nano_') } catch (err) { console.error(err) @@ -172,16 +149,9 @@ export class Rolodex { */ static async getName (address: string): Promise { try { - const response = await SafeWorker.request({ - method: 'fetch', - names: address, - store: 'Rolodex', - password: utf8.toBuffer('') - }) + const response = await Database.get(address, this.#DB_NAME) const name = response[address] return name - ? bytes.toUtf8(new Uint8Array(name)) - : null } catch (err) { console.error(err) return null diff --git a/src/lib/safe/passkey.ts b/src/lib/safe/passkey.ts index 196d6b4..6bcb981 100644 --- a/src/lib/safe/passkey.ts +++ b/src/lib/safe/passkey.ts @@ -3,25 +3,29 @@ 'use strict' -import { bytes } from '#src/lib/convert.js' +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?: string + 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, data } = this.#extractData(message.data) + const { action, type, password, iv, salt, seed, mnemonic, index, encrypted, data } = this.#extractData(message.data) try { let result: NamedData switch (action) { @@ -34,11 +38,11 @@ export class Passkey { break } case 'derive': { - result = await this.derive(index) + result = await this.derive(type, index) break } - case 'backup': { - result = await this.backup() + case 'import': { + result = await this.import(type, password, seed, mnemonic) break } case 'lock': { @@ -46,10 +50,15 @@ export class Passkey { break } case 'sign': { - result = await this sign(data) + result = await this.sign(index, data) + break } case 'unlock': { - result = await this.unlock(password, iv, salt) + result = await this.unlock(password, iv, salt, encrypted) + break + } + case 'verify': { + result = await this.verify(seed, mnemonic) break } default: { @@ -77,113 +86,131 @@ export class Passkey { NODE: this.#parentPort?.on('message', listener) } - static async create (type: 'BIP-44' | 'BLAKE2b', password: ArrayBuffer, salt?: ArrayBuffer): Promise> { + static async create (type?: 'BIP-44' | 'BLAKE2b', password?: ArrayBuffer, salt?: ArrayBuffer): Promise { try { - const key = await this.#createAesKey('encrypt', password, this.#salt.buffer) - const iv = crypto.getRandomValues(new Uint8Array(32)) - const entropy = crypto.getRandomValues(new Uint8Array(32)) - const m = await this.#bip39Mnemonic(entropy) - const s = await m.toBip39Seed(salt) - this.#type = type - return { iv, salt: this.#salt.buffer } + 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 import wallet', { cause: err }) + throw new Error('Failed to unlock wallet', { cause: err }) } } - static async derive (password: ArrayBuffer, seed: ArrayBuffer, mnemonic?: string, salt?: ArrayBuffer): Promise> { + static async derive (type: 'BIP-44' | 'BLAKE2b', index: number): Promise> { try { - const key = await this.#createAesKey('encrypt', password, this.#salt.buffer) - return { isImported: true } + 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 import wallet', { cause: err }) + throw new Error('Failed to derive account', { cause: err }) } } - /** - * Returns the seed and, if it exists, the mnemonic. The wallet must be - * unlocked prior to backup. - */ - static async backup (): Promise> { + static async import (type?: 'BIP-44' | 'BLAKE2b', password?: ArrayBuffer, seed?: ArrayBuffer, mnemonic?: string, salt?: ArrayBuffer): Promise { try { - if (this.#seed == null) { - throw new Error('Wallet is locked') + if (type == null) { + throw new TypeError('Wallet type is required') } - const result: NamedData = {} - if (this.#mnemonic != null) { - result.mnemonic = this.#mnemonic + if (password == null) { + throw new TypeError('Wallet password is required') } - return { - ...result, - seed: bytes.toHex(this.#seed) + 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 export wallet', { cause: err }) + throw new Error('Failed to import wallet', { cause: err }) } } - static async lock (): Promise> { - try { - this.#mnemonic = undefined - this.#seed = undefined - return { isLocked: this.#mnemonic === undefined && this.#seed === undefined } - } catch (err) { - throw new Error('Failed to lock wallet', { cause: err }) - } + static async lock (): Promise { + this.#mnemonic = undefined + this.#seed = undefined + this.#locked = true } - static async sign (): Promise> { + /** + * 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 { - this.#mnemonic = undefined - this.#seed = undefined - return { isLocked: this.#mnemonic === undefined && this.#seed === undefined } + 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 lock wallet', { cause: err }) + throw new Error('Failed to sign message', { cause: err }) } } - static async unlock (encrypted: ArrayBuffer, password: ArrayBuffer, iv: ArrayBuffer, salt: ArrayBuffer): Promise> { + /** + * 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) - return { isUnlocked: true } + 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 }) } } - 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 (!('password' in dataObject) || !(dataObject.password instanceof ArrayBuffer)) { - throw new TypeError('Password must be ArrayBuffer') - } - const password: ArrayBuffer = dataObject.password - if (!('action' in dataObject)) { - throw new TypeError('Wallet action is required') - } - if (dataObject.action !== 'STOP' - && dataObject.action !== 'create' - && dataObject.action !== 'derive' - && dataObject.action !== 'export' - && dataObject.action !== 'import' - && dataObject.action !== 'lock' - && dataObject.action !== 'sign' - && dataObject.action !== 'unlock') { - throw new TypeError('Invalid wallet action') - } - const action = dataObject.action - if (action === 'unlock' && !(dataObject.salt instanceof ArrayBuffer)) { - throw new TypeError('Salt required for decryption key to unlock') + /** + * 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 }) } - const salt: ArrayBuffer = action === 'unlock' && dataObject.salt instanceof ArrayBuffer - ? dataObject.salt - : crypto.getRandomValues(new Uint8Array(32)).buffer - return { action, type, password, iv, seed, mnemonic, salt, index, data } } static async #bip39Mnemonic (entropy: Uint8Array) { @@ -226,19 +253,83 @@ export class Passkey { return await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose]) } - static async #encryptWallet (salt: ArrayBuffer) { - const data: NamedData = { - mnemonic: this.#mnemonic, - seed: this.#seed + 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 encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv }, encryptionKey, data[label]) - const record = { - iv: iv, - salt: salt, - label, - encrypted + 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 } } } diff --git a/src/lib/wallets/bip44-wallet.ts b/src/lib/wallets/bip44-wallet.ts index 3849232..242777f 100644 --- a/src/lib/wallets/bip44-wallet.ts +++ b/src/lib/wallets/bip44-wallet.ts @@ -7,6 +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' /** * Hierarchical deterministic (HD) wallet created by using a source of entropy to @@ -179,7 +180,9 @@ export class Bip44Wallet extends Wallet { if (this.isLocked) { throw new Error('wallet must be unlocked to derive accounts') } - const results = await Bip44CkdWorker.request({ + const results = await PasskeyWorker.request({ + action: 'derive', + type: 'BIP-44', indexes, seed: hex.toBuffer(this.seed) }) -- 2.47.3