]> git.codecow.com Git - libnemo.git/commitdiff
Replace async-await with Promise chains.
authorChris Duncan <chris@zoso.dev>
Sat, 30 Aug 2025 05:23:06 +0000 (22:23 -0700)
committerChris Duncan <chris@zoso.dev>
Sat, 30 Aug 2025 05:23:06 +0000 (22:23 -0700)
src/lib/vault/index.ts
src/lib/vault/vault-worker.ts

index 9868052fc44669d5505ef7470462266d882def74..39bdce976b5cb50e2c260cf388f2d8e3e570aeb2 100644 (file)
@@ -62,11 +62,11 @@ export class Vault {
                Vault.#instances.push(this)
        }
 
-       async prioritize<T extends Data> (data: NamedData): Promise<NamedData<T>> {
+       prioritize<T extends Data> (data: NamedData): Promise<NamedData<T>> {
                return this.#assign<T>(data, task => this.#queue.unshift(task))
        }
 
-       async request<T extends Data> (data: NamedData): Promise<NamedData<T>> {
+       request<T extends Data> (data: NamedData): Promise<NamedData<T>> {
                return this.#assign<T>(data, task => this.#queue.push(task))
        }
 
@@ -76,8 +76,8 @@ export class Vault {
                this.#isTerminated = true
        }
 
-       async #assign<T extends Data> (data: NamedData, enqueue: (task: Task) => number): Promise<NamedData<T>> {
-               return new Promise(async (resolve, reject): Promise<void> => {
+       #assign<T extends Data> (data: NamedData, enqueue: (task: Task) => number): Promise<NamedData<T>> {
+               return new Promise((resolve, reject): void => {
                        if (this.#isTerminated) {
                                reject('Worker terminated')
                        }
@@ -87,7 +87,7 @@ export class Vault {
                                resolve,
                                reject
                        }
-                       await enqueue(task)
+                       enqueue(task)
                        if (this.#isIdle) this.#process()
                })
        }
index 2f4efb869938f870d69bae39afc3108f470456fd..57840a65f445e8f56e6103d1bee24dd81779e64a 100644 (file)
@@ -24,8 +24,8 @@ export class VaultWorker {
 
        static {
                NODE: this.#parentPort = parentPort
-               const listener = async (message: MessageEvent<any>): Promise<void> => {
-                       try {
+               const listener = (message: MessageEvent<any>): Promise<void> => {
+                       return this.#extractData(message.data).then(extracted => {
                                const {
                                        action,
                                        type,
@@ -38,70 +38,73 @@ export class VaultWorker {
                                        index,
                                        encrypted,
                                        data
-                               } = await this.#extractData(message.data)
-                               let result: NamedData
+                               } = extracted
+                               let result: Promise<NamedData>
                                switch (action) {
                                        case 'STOP': {
                                                BROWSER: close()
                                                NODE: process.exit()
                                        }
                                        case 'create': {
-                                               result = await this.create(type, key, keySalt, mnemonicSalt)
+                                               result = this.create(type, key, keySalt, mnemonicSalt)
                                                break
                                        }
                                        case 'derive': {
-                                               result = await this.derive(index)
+                                               result = this.derive(index)
                                                break
                                        }
                                        case 'load': {
-                                               result = await this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+                                               result = this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
                                                break
                                        }
                                        case 'lock': {
-                                               result = this.lock()
+                                               result = Promise.resolve(this.lock())
                                                break
                                        }
                                        case 'sign': {
-                                               result = await this.sign(index, data)
+                                               result = this.sign(index, data)
                                                break
                                        }
                                        case 'unlock': {
-                                               result = await this.unlock(type, key, iv, encrypted)
+                                               result = this.unlock(type, key, iv, encrypted)
                                                break
                                        }
                                        case 'update': {
-                                               result = await this.update(key, keySalt)
+                                               result = this.update(key, keySalt)
                                                break
                                        }
                                        case 'verify': {
-                                               result = this.verify(seed, mnemonicPhrase)
+                                               result = Promise.resolve(this.verify(seed, mnemonicPhrase))
                                                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])
+                               return result.then(result => {
+                                       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) {
-                               console.error(err)
-                               for (const key of Object.keys(message.data)) {
-                                       if (message.data[key] instanceof ArrayBuffer && !message.data[key].detached) {
-                                               new Uint8Array(message.data[key]).fill(0).buffer.transfer?.()
+                                       //@ts-expect-error
+                                       BROWSER: postMessage(result, transfer)
+                                       //@ts-expect-error
+                                       NODE: parentPort?.postMessage(result, transfer)
+                               })
+                       })
+                               .catch((err: any) => {
+                                       console.error(err)
+                                       for (const key of Object.keys(message.data)) {
+                                               if (message.data[key] instanceof ArrayBuffer && !message.data[key].detached) {
+                                                       new Uint8Array(message.data[key]).fill(0).buffer.transfer?.()
+                                               }
+                                               message.data[key] = undefined
                                        }
-                                       message.data[key] = undefined
-                               }
-                               BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err })
-                               NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err })
-                       }
+                                       BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err })
+                                       NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err })
+                               })
                }
                BROWSER: addEventListener('message', listener)
                NODE: this.#parentPort?.on('message', listener)
@@ -111,15 +114,19 @@ export class VaultWorker {
        * 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 create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+       static create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
                        const entropy = crypto.getRandomValues(new Uint8Array(32))
-                       const { phrase: mnemonicPhrase } = await Bip39.fromEntropy(entropy)
-                       const record = await this.#load(type, key, keySalt, mnemonicPhrase, mnemonicSalt)
-                       if (this.#seed == null || this.#mnemonic == null) {
-                               throw new Error('Failed to generate seed and mnemonic')
-                       }
-                       return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
+                       return Bip39.fromEntropy(entropy)
+                               .then(bip39 => {
+                                       return this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt)
+                                               .then(record => {
+                                                       if (this.#seed == null || this.#mnemonic == null) {
+                                                               throw new Error('Failed to generate seed and mnemonic')
+                                                       }
+                                                       return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
+                                               })
+                               })
                } catch (err) {
                        console.error(err)
                        throw new Error('Failed to create wallet', { cause: err })
@@ -133,8 +140,9 @@ export class VaultWorker {
        * wallet seed at a specified index and then returns the public key. The wallet
        * must be unlocked prior to derivation.
        */
-       static async derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
+       static derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
                try {
+                       this.#timeout.pause()
                        if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
@@ -147,13 +155,14 @@ export class VaultWorker {
                        if (typeof index !== 'number') {
                                throw new Error('Invalid wallet account index')
                        }
-                       this.#timeout.pause()
-                       const prv = this.#type === 'BIP-44'
-                               ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
-                               : await this.#deriveBlake2bPrivateKey(this.#seed, index)
-                       const pub = await NanoNaCl.convert(new Uint8Array(prv))
-                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
-                       return { index, publicKey: pub.buffer }
+                       const derive = this.#type === 'BIP-44'
+                               ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
+                               : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index))
+                       return derive.then(prv => {
+                               const pub = NanoNaCl.convert(new Uint8Array(prv))
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
+                               return { index, publicKey: pub.buffer }
+                       })
                } catch (err) {
                        console.error(err)
                        this.#timeout.resume()
@@ -165,19 +174,19 @@ export class VaultWorker {
        * 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 load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
-               try {
-                       const record = await this.#load(type, key, keySalt, secret, mnemonicSalt)
-                       if (this.#seed == null) {
-                               throw new Error('Wallet seed not found')
-                       }
-                       return record
-               } catch (err) {
-                       console.error(err)
-                       throw new Error('Failed to load wallet', { cause: err })
-               } finally {
-                       this.lock()
-               }
+       static load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+               return this.#load(type, key, keySalt, secret, mnemonicSalt)
+                       .then(record => {
+                               if (this.#seed == null) {
+                                       throw new Error('Wallet seed not found')
+                               }
+                               return record
+                       })
+                       .catch(err => {
+                               console.error(err)
+                               throw new Error('Failed to load wallet', { cause: err })
+                       })
+                       .finally(() => this.lock())
        }
 
        static lock (): NamedData<boolean> {
@@ -192,8 +201,9 @@ export class VaultWorker {
        * 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>> {
+       static sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
                try {
+                       this.#timeout.pause()
                        if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
@@ -206,13 +216,14 @@ export class VaultWorker {
                        if (data == null) {
                                throw new Error('Data to sign not found')
                        }
-                       this.#timeout.pause()
-                       const prv = this.#type === 'BIP-44'
-                               ? await Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
-                               : await this.#deriveBlake2bPrivateKey(this.#seed, index)
-                       const sig = await NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv))
-                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
-                       return { signature: sig.buffer }
+                       const derive = this.#type === 'BIP-44'
+                               ? Bip44.ckd(this.#seed, BIP44_COIN_NANO, index)
+                               : Promise.resolve(this.#deriveBlake2bPrivateKey(this.#seed, index))
+                       return derive.then(prv => {
+                               const sig = NanoNaCl.detached(new Uint8Array(data), new Uint8Array(prv))
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
+                               return { signature: sig.buffer }
+                       })
                } catch (err) {
                        console.error(err)
                        this.#timeout.resume()
@@ -223,43 +234,45 @@ export class VaultWorker {
        /**
        * Decrypts the input and sets the seed and, if it is included, the mnemonic.
        */
-       static async unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
-               try {
-                       if (type == null) {
-                               throw new TypeError('Wallet type is required')
-                       }
-                       if (key == null) {
-                               throw new TypeError('Wallet password is required')
-                       }
-                       if (iv == null) {
-                               throw new TypeError('Wallet IV is required')
-                       }
-                       if (encrypted == null) {
-                               throw new TypeError('Wallet encrypted data is required')
-                       }
-                       this.#timeout?.pause()
-                       await this.#decryptWallet(type, key, iv, encrypted)
-                       if (!(this.#seed instanceof ArrayBuffer)) {
-                               throw new TypeError('Invalid seed')
-                       }
-                       if (this.#mnemonic != null && !(this.#mnemonic instanceof ArrayBuffer)) {
-                               throw new TypeError('Invalid mnemonic')
-                       }
-                       this.#locked = false
-                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
-                       return { isUnlocked: !this.#locked }
-               } catch (err) {
-                       console.error(err)
-                       this.#timeout?.resume()
-                       throw new Error('Failed to unlock wallet', { cause: err })
+       static unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
+               if (type == null) {
+                       throw new TypeError('Wallet type is required')
+               }
+               if (key == null) {
+                       throw new TypeError('Wallet password is required')
                }
+               if (iv == null) {
+                       throw new TypeError('Wallet IV is required')
+               }
+               if (encrypted == null) {
+                       throw new TypeError('Wallet encrypted data is required')
+               }
+               this.#timeout?.pause()
+               return this.#decryptWallet(type, key, iv, encrypted)
+                       .then(() => {
+                               if (!(this.#seed instanceof ArrayBuffer)) {
+                                       throw new TypeError('Invalid seed')
+                               }
+                               if (this.#mnemonic != null && !(this.#mnemonic instanceof ArrayBuffer)) {
+                                       throw new TypeError('Invalid mnemonic')
+                               }
+                               this.#locked = false
+                               this.#timeout = new VaultTimer(() => this.lock(), 120000)
+                               return { isUnlocked: !this.#locked }
+                       })
+                       .catch(err => {
+                               console.error(err)
+                               this.#timeout?.resume()
+                               throw new Error('Failed to unlock wallet', { cause: err })
+                       })
        }
 
        /**
        * Re-encrypts the wallet with a new password.
        */
-       static async update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
+       static update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
                try {
+                       this.#timeout.pause()
                        if (this.#locked) {
                                throw new Error('Wallet is locked')
                        }
@@ -269,10 +282,11 @@ export class VaultWorker {
                        if (key == null || keySalt == null) {
                                throw new TypeError('Wallet password is required')
                        }
-                       this.#timeout.pause()
-                       const { iv, encrypted } = await this.#encryptWallet(key)
-                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
-                       return { iv, salt: keySalt, encrypted }
+                       return this.#encryptWallet(key)
+                               .then(({ iv, encrypted }) => {
+                                       this.#timeout = new VaultTimer(() => this.lock(), 120000)
+                                       return { iv, salt: keySalt, encrypted }
+                               })
                } catch (err) {
                        console.error(err)
                        this.#timeout.resume()
@@ -324,29 +338,46 @@ export class VaultWorker {
                }
        }
 
-       static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise<CryptoKey> {
-               const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveKey'])
-               new Uint8Array(password).fill(0).buffer.transfer?.()
-               const derivationAlgorithm: Pbkdf2Params = {
-                       name: 'PBKDF2',
-                       hash: 'SHA-512',
-                       iterations: 210000,
-                       salt: keySalt
-               }
-               const derivedKeyType: AesKeyGenParams = {
-                       name: 'AES-GCM',
-                       length: 256
-               }
-               return await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
+       static #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
+               return new Promise((resolve, reject): void => {
+                       if (password == null) {
+                               resolve(undefined)
+                               return
+                       }
+                       try {
+                               crypto.subtle
+                                       .importKey('raw', password, 'PBKDF2', false, ['deriveKey'])
+                                       .then(derivationKey => {
+                                               new Uint8Array(password).fill(0).buffer.transfer?.()
+                                               const derivationAlgorithm: Pbkdf2Params = {
+                                                       name: 'PBKDF2',
+                                                       hash: 'SHA-512',
+                                                       iterations: 210000,
+                                                       salt: keySalt
+                                               }
+                                               const derivedKeyType: AesKeyGenParams = {
+                                                       name: 'AES-GCM',
+                                                       length: 256
+                                               }
+                                               crypto.subtle
+                                                       .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
+                                                       .then(resolve)
+                                       })
+                       } catch (err) {
+                               reject(err)
+                       }
+               })
        }
 
-       static async #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
+       static #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
                const seedLength = type === 'BIP-44' ? 64 : 32
                const additionalData = utf8.toBytes(type)
-               const decrypted = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted))
-               this.#seed = decrypted.buffer.slice(0, seedLength)
-               this.#mnemonic = decrypted.buffer.slice(seedLength)
-               decrypted.fill(0)
+               return crypto.subtle.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
+                       .then(decrypted => {
+                               this.#seed = decrypted.slice(0, seedLength)
+                               this.#mnemonic = decrypted.slice(seedLength)
+                               new Uint8Array(decrypted).fill(0)
+                       })
        }
 
        /**
@@ -370,7 +401,7 @@ export class VaultWorker {
                return new Blake2b(32).update(s).update(i).digest().buffer
        }
 
-       static async #encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
+       static #encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
                if (this.#type == null) {
                        throw new Error('Invalid wallet type')
                }
@@ -383,164 +414,175 @@ export class VaultWorker {
                const iv = crypto.getRandomValues(new Uint8Array(12)).buffer
                const additionalData = utf8.toBytes(this.#type)
                const encoded = new Uint8Array([...seed, ...mnemonic])
-               const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
-               encoded.fill(0)
-               return { iv, encrypted }
+               return crypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
+                       .then(encrypted => {
+                               encoded.fill(0)
+                               return { iv, encrypted }
+                       })
        }
 
        /**
        * 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')
-               }
-               if (typeof message !== 'object') {
-                       throw new Error('Invalid data')
-               }
-               const messageData = message as { [key: string]: unknown }
-
-               // 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 !== 'load'
-                       && messageData.action !== 'lock'
-                       && messageData.action !== 'sign'
-                       && messageData.action !== 'unlock'
-                       && messageData.action !== 'update'
-                       && messageData.action !== 'verify') {
-                       throw new TypeError('Invalid wallet action')
-               }
-               const action = messageData.action
-
-               // Password for lock/unlock key
-               if (messageData.password != null && !(messageData.password instanceof ArrayBuffer)) {
-                       throw new TypeError('Password must be ArrayBuffer')
-               }
-               let password = messageData.password?.slice()
-               if (messageData.password instanceof ArrayBuffer) {
-                       new Uint8Array(messageData.password).fill(0)
-                       delete 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 = password instanceof ArrayBuffer
-                       ? await this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', password, keySalt)
-                       : undefined
-               if (password instanceof ArrayBuffer && !password.detached) {
-                       new Uint8Array(password).fill(0)
-                       password = undefined
-               }
-
-               // 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
-
-               // Import requires seed or mnemonic phrase
-               if (action === 'load' && messageData.seed == null && messageData.mnemonicPhrase == null) {
-                       throw new TypeError('Seed or mnemonic phrase required to load wallet')
-               }
+       static #extractData (message: unknown) {
+               try {
+                       // Message itself
+                       if (message == null) {
+                               throw new TypeError('Worker received no data')
+                       }
+                       if (typeof message !== 'object') {
+                               throw new Error('Invalid data')
+                       }
+                       const messageData = message as { [key: string]: unknown }
+
+                       // 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 !== 'load'
+                               && messageData.action !== 'lock'
+                               && messageData.action !== 'sign'
+                               && messageData.action !== 'unlock'
+                               && messageData.action !== 'update'
+                               && messageData.action !== 'verify') {
+                               throw new TypeError('Invalid wallet action')
+                       }
+                       const action = messageData.action
+
+                       // Password for lock/unlock key
+                       if (messageData.password != null && !(messageData.password instanceof ArrayBuffer)) {
+                               throw new TypeError('Password must be ArrayBuffer')
+                       }
+                       let password = messageData.password?.slice()
+                       if (messageData.password instanceof ArrayBuffer) {
+                               new Uint8Array(messageData.password).fill(0)
+                               delete 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
+                       return this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', keySalt, password)
+                               .then(key => {
+                                       if (password?.detached === false) {
+                                               new Uint8Array(password).fill(0)
+                                               password = undefined
+                                       }
 
-               // Seed to load
-               if (action === 'load' && 'seed' in messageData && !(messageData.seed instanceof ArrayBuffer)) {
-                       throw new TypeError('Seed required to load wallet')
-               }
-               const seed = messageData.seed instanceof ArrayBuffer
-                       ? messageData.seed.slice()
-                       : undefined
-               if (messageData.seed instanceof ArrayBuffer) {
-                       new Uint8Array(messageData.seed).fill(0)
-                       delete messageData.seed
-               }
+                                       // 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
 
-               // Mnemonic phrase to load
-               if (action === 'load' && 'mnemonicPhrase' in message && typeof messageData.mnemonicPhrase !== 'string') {
-                       throw new TypeError('Invalid mnemonic phrase')
-               }
-               const mnemonicPhrase = typeof messageData.mnemonicPhrase === 'string'
-                       ? messageData.mnemonicPhrase
-                       : undefined
-               delete messageData.mnemonicPhrase
-
-               // Mnemonic salt for mnemonic phrase to load
-               if (action === 'load' && 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
-               delete messageData.mnemonicSalt
+                                       // Import requires seed or mnemonic phrase
+                                       if (action === 'load' && messageData.seed == null && messageData.mnemonicPhrase == null) {
+                                               throw new TypeError('Seed or mnemonic phrase required to load wallet')
+                                       }
 
-               // Encrypted seed and possibly mnemonic
-               if (action === 'unlock') {
-                       if (messageData.encrypted == null) {
-                               throw new TypeError('Wallet encrypted secrets not found')
-                       }
-                       if (!(messageData.encrypted instanceof ArrayBuffer)) {
-                               throw new TypeError('Invalid wallet encrypted secrets')
-                       }
-               }
-               const encrypted = messageData.encrypted instanceof ArrayBuffer
-                       ? messageData.encrypted.slice()
-                       : undefined
-               if (messageData.encrypted instanceof ArrayBuffer) {
-                       new Uint8Array(messageData.encrypted).fill(0)
-                       delete messageData.encrypted
-               }
+                                       // Seed to load
+                                       if (action === 'load' && 'seed' in messageData && !(messageData.seed instanceof ArrayBuffer)) {
+                                               throw new TypeError('Seed required to load wallet')
+                                       }
+                                       const seed = messageData.seed instanceof ArrayBuffer
+                                               ? messageData.seed.slice()
+                                               : undefined
+                                       if (messageData.seed instanceof ArrayBuffer) {
+                                               new Uint8Array(messageData.seed).fill(0)
+                                               delete messageData.seed
+                                       }
 
-               // 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
+                                       // Mnemonic phrase to load
+                                       if (action === 'load' && 'mnemonicPhrase' in message && typeof messageData.mnemonicPhrase !== 'string') {
+                                               throw new TypeError('Invalid mnemonic phrase')
+                                       }
+                                       const mnemonicPhrase = typeof messageData.mnemonicPhrase === 'string'
+                                               ? messageData.mnemonicPhrase
+                                               : undefined
+                                       delete messageData.mnemonicPhrase
+
+                                       // Mnemonic salt for mnemonic phrase to load
+                                       if (action === 'load' && 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
+                                       delete messageData.mnemonicSalt
+
+                                       // Encrypted seed and possibly mnemonic
+                                       if (action === 'unlock') {
+                                               if (messageData.encrypted == null) {
+                                                       throw new TypeError('Wallet encrypted secrets not found')
+                                               }
+                                               if (!(messageData.encrypted instanceof ArrayBuffer)) {
+                                                       throw new TypeError('Invalid wallet encrypted secrets')
+                                               }
+                                       }
+                                       const encrypted = messageData.encrypted instanceof ArrayBuffer
+                                               ? messageData.encrypted.slice()
+                                               : undefined
+                                       if (messageData.encrypted instanceof ArrayBuffer) {
+                                               new Uint8Array(messageData.encrypted).fill(0)
+                                               delete messageData.encrypted
+                                       }
 
-               // Data to sign
-               if (action === 'sign') {
-                       if (messageData.data == null) {
-                               throw new TypeError('Data to sign not found')
-                       }
-                       if (!(messageData.data instanceof ArrayBuffer)) {
-                               throw new TypeError('Invalid data to sign')
-                       }
+                                       // 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')
+                                               }
+                                               if (!(messageData.data instanceof ArrayBuffer)) {
+                                                       throw new TypeError('Invalid data to sign')
+                                               }
+                                       }
+                                       const data = messageData.data instanceof ArrayBuffer
+                                               ? messageData.data
+                                               : undefined
+                                       delete messageData.data
+
+                                       return { action, type, key, iv, keySalt, seed, mnemonicPhrase, mnemonicSalt, encrypted, index, data }
+                               })
+                               .catch(err => {
+                                       console.error(err)
+                                       throw new Error('Failed to create AES CryptoKey', { cause: err })
+                               })
+               } catch (err) {
+                       console.error(err)
+                       throw new Error('Failed to extract data', { cause: err })
                }
-               const data = messageData.data instanceof ArrayBuffer
-                       ? messageData.data
-                       : undefined
-               delete messageData.data
-
-               return { action, type, key, iv, keySalt, seed, mnemonicPhrase, mnemonicSalt, encrypted, index, data }
        }
 
        /**
        * 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 #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+       static #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
                try {
                        if (!this.#locked) {
                                throw new Error('Wallet is in use')
@@ -571,20 +613,34 @@ export class VaultWorker {
                                }
                        }
                        this.#type = type
+                       let seed: Promise<ArrayBuffer>
                        if (secret instanceof ArrayBuffer) {
-                               this.#seed = secret
                                if (type === 'BLAKE2b') {
-                                       this.#mnemonic = utf8.toBuffer((await Bip39.fromEntropy(new Uint8Array(secret))).phrase ?? '')
+                                       seed = Bip39.fromEntropy(new Uint8Array(secret))
+                                               .then(bip39 => {
+                                                       this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+                                                       return secret
+                                               })
+                               } else {
+                                       seed = Promise.resolve(secret)
                                }
                        } else {
-                               const mnemonic = await Bip39.fromPhrase(secret)
-                               this.#mnemonic = utf8.toBuffer(mnemonic.phrase ?? '')
-                               this.#seed = type === 'BIP-44'
-                                       ? (await mnemonic.toBip39Seed(mnemonicSalt ?? '')).buffer
-                                       : (await mnemonic.toBlake2bSeed()).buffer
-                       }
-                       const { iv, encrypted } = await this.#encryptWallet(key)
-                       return { iv, salt: keySalt, encrypted }
+                               seed = Bip39.fromPhrase(secret)
+                                       .then(bip39 => {
+                                               this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+                                               const derive = type === 'BIP-44'
+                                                       ? bip39.toBip39Seed(mnemonicSalt ?? '')
+                                                       : Promise.resolve(bip39.toBlake2bSeed())
+                                               return derive.then(s => s.buffer)
+                                       })
+                       }
+                       return seed.then(seed => {
+                               this.#seed = seed
+                               return this.#encryptWallet(key)
+                                       .then(({ iv, encrypted }) => {
+                                               return { iv, salt: keySalt, encrypted }
+                                       })
+                       })
                } catch (err) {
                        throw new Error('Failed to load wallet', { cause: err })
                }