]> git.codecow.com Git - libnemo.git/commitdiff
Refactor data extraction in vault worker.
authorChris Duncan <chris@zoso.dev>
Wed, 3 Sep 2025 05:44:38 +0000 (22:44 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 3 Sep 2025 05:44:38 +0000 (22:44 -0700)
src/lib/vault/vault-worker.ts
src/lib/wallet/sign.ts
src/lib/wallet/update.ts
test/test.tools.mjs

index d0554af335a833f927cc349b652960b4b7a5fde7..fb49762965d64c27df9277eb92d006b76e1613c3 100644 (file)
@@ -26,84 +26,69 @@ export class VaultWorker {
                this.#seed = undefined
                this.#mnemonic = undefined
                NODE: this.#parentPort = parentPort
-               const listener = (message: MessageEvent<any>): Promise<void> => {
-                       return this.#extractData(message.data)
-                               .then(extracted => {
-                                       const {
-                                               action,
-                                               type,
-                                               key,
-                                               keySalt,
-                                               iv,
-                                               seed,
-                                               mnemonicPhrase,
-                                               mnemonicSalt,
-                                               index,
-                                               encrypted,
-                                               data
-                                       } = extracted
-                                       let result: Promise<NamedData>
+               const listener = (event: MessageEvent<any>): void => {
+                       const data = this.#parseData(event.data)
+                       const action = this.#parseAction(data)
+                       const type = this.#parseType(action, data)
+                       const keySalt = this.#parseKeySalt(action, data)
+                       const iv = this.#parseIv(action, data)
+                       const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message } = this.#extractData(action, data)
+                       this.#createPasskey(action, keySalt, data)
+                               .then((key: CryptoKey | undefined): Promise<NamedData> => {
                                        switch (action) {
                                                case 'STOP': {
                                                        BROWSER: close()
                                                        NODE: process.exit()
                                                }
                                                case 'create': {
-                                                       result = this.create(type, key, keySalt, mnemonicSalt)
-                                                       break
+                                                       return this.create(type, key, keySalt, mnemonicSalt)
                                                }
                                                case 'derive': {
-                                                       result = this.derive(index)
-                                                       break
+                                                       return this.derive(index)
                                                }
                                                case 'load': {
-                                                       result = this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+                                                       return this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
                                                        break
                                                }
                                                case 'lock': {
-                                                       result = Promise.resolve(this.lock())
-                                                       break
+                                                       return Promise.resolve(this.lock())
                                                }
                                                case 'sign': {
-                                                       result = this.sign(index, data)
-                                                       break
+                                                       return this.sign(index, message)
                                                }
                                                case 'unlock': {
-                                                       result = this.unlock(type, key, iv, encrypted)
-                                                       break
+                                                       return this.unlock(type, key, iv, encrypted)
                                                }
                                                case 'update': {
-                                                       result = this.update(key, keySalt)
-                                                       break
+                                                       return this.update(key, keySalt)
                                                }
                                                case 'verify': {
-                                                       result = Promise.resolve(this.verify(seed, mnemonicPhrase))
-                                                       break
+                                                       return Promise.resolve(this.verify(seed, mnemonicPhrase))
                                                }
                                                default: {
                                                        throw new Error(`Unknown wallet action '${action}'`)
                                                }
                                        }
-                                       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])
-                                                       }
+                               })
+                               .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)
-                                       })
+                                       }
+                                       //@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?.()
+                                       for (const key of Object.keys(event.data)) {
+                                               if (event.data[key] instanceof ArrayBuffer && !event.data[key].detached) {
+                                                       new Uint8Array(event.data[key]).fill(0).buffer.transfer?.()
                                                }
-                                               message.data[key] = undefined
+                                               event.data[key] = undefined
                                        }
                                        BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err })
                                        NODE: parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err })
@@ -340,30 +325,49 @@ export class VaultWorker {
                }
        }
 
-       #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
-               if (password == null) {
+       #createPasskey (action: string, salt: ArrayBuffer, data: { [key: string]: unknown }) {
+               // Allowlisted wallet actions
+               if (['create', 'load', 'unlock', 'update'].includes(action)) {
+
+                       // Create local copy of password ASAP, then clear bytes from original buffer
+                       if (!(data.password instanceof ArrayBuffer)) {
+                               throw new TypeError('Password must be ArrayBuffer')
+                       }
+
+                       const password = data.password.slice()
+                       new Uint8Array(data.password).fill(0)
+                       delete data.password
+
+                       // Only unlocking should decrypt the vault; other sensitive actions should
+                       // throw if the vault is still locked and encrypted
+                       const purpose = action === 'unlock' ? 'decrypt' : 'encrypt'
+
+                       return 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
+                                       }
+                                       const derivedKeyType: AesKeyGenParams = {
+                                               name: 'AES-GCM',
+                                               length: 256
+                                       }
+                                       return crypto.subtle
+                                               .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
+                               })
+                               .catch(err => {
+                                       console.error(err)
+                                       throw new Error('Failed to derive CryptoKey from password', { cause: err })
+                               })
+               } else if (data.password !== undefined) {
+                       throw new Error('Password is not allowed for this action', { cause: action })
+               } else {
                        return Promise.resolve(undefined)
                }
-               return 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
-                               }
-                               return crypto.subtle
-                                       .deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
-                       })
-                       .catch(err => {
-                               throw new Error(err)
-                       })
        }
 
        #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
@@ -423,154 +427,83 @@ export class VaultWorker {
        /**
        * Parse inbound message from main thread into typechecked variables.
        */
-       #extractData (message: unknown) {
+       #extractData (action: string, data: { [key: string]: 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
+                       // Import requires seed or mnemonic phrase
+                       if (action === 'load' && data.seed == null && data.mnemonicPhrase == null) {
+                               throw new TypeError('Seed or mnemonic phrase required to load wallet')
                        }
 
-                       // 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')
+                       // Seed to load
+                       if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) {
+                               throw new TypeError('Seed required to load 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 seed = data.seed instanceof ArrayBuffer
+                               ? data.seed.slice()
+                               : undefined
+                       if (data.seed instanceof ArrayBuffer) {
+                               new Uint8Array(data.seed).fill(0)
+                               delete data.seed
                        }
-                       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
-                                       }
+                       // Mnemonic phrase to load
+                       if (action === 'load' && 'mnemonicPhrase' in data && typeof data.mnemonicPhrase !== 'string') {
+                               throw new TypeError('Invalid mnemonic phrase')
+                       }
+                       const mnemonicPhrase = typeof data.mnemonicPhrase === 'string'
+                               ? data.mnemonicPhrase
+                               : undefined
+                       delete data.mnemonicPhrase
 
-                                       // 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')
-                                       }
+                       // Mnemonic salt for mnemonic phrase to load
+                       if (action === 'load' && data.mnemonicSalt != undefined && typeof data.mnemonicSalt !== 'string') {
+                               throw new TypeError('Invalid mnemonic salt for mnemonic phrase')
+                       }
+                       const mnemonicSalt = typeof data.mnemonicSalt === 'string'
+                               ? data.mnemonicSalt
+                               : undefined
+                       delete data.mnemonicSalt
 
-                                       // 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
-                                       }
+                       // Encrypted seed and possibly mnemonic
+                       if (action === 'unlock') {
+                               if (data.encrypted == null) {
+                                       throw new TypeError('Wallet encrypted secrets not found')
+                               }
+                               if (!(data.encrypted instanceof ArrayBuffer)) {
+                                       throw new TypeError('Invalid wallet encrypted secrets')
+                               }
+                       }
+                       const encrypted = data.encrypted instanceof ArrayBuffer
+                               ? data.encrypted.slice()
+                               : undefined
+                       if (data.encrypted instanceof ArrayBuffer) {
+                               new Uint8Array(data.encrypted).fill(0)
+                               delete data.encrypted
+                       }
 
-                                       // 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
-                                       }
+                       // Index for child account to derive or sign
+                       if ((action === 'derive' || action === 'sign') && typeof data.index !== 'number') {
+                               throw new TypeError('Index is required to derive an account private key')
+                       }
+                       const index = typeof data.index === 'number'
+                               ? data.index
+                               : undefined
 
-                                       // 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
+                       // Data to sign
+                       if (action === 'sign') {
+                               if (data.message == null) {
+                                       throw new TypeError('Data to sign not found')
+                               }
+                               if (!(data.message instanceof ArrayBuffer)) {
+                                       throw new TypeError('Invalid data to sign')
+                               }
+                       }
+                       const message = data.message instanceof ArrayBuffer
+                               ? data.message
+                               : undefined
+                       delete data.message
 
-                                       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 })
-                               })
+                       return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message }
                } catch (err) {
                        console.error(err)
                        throw new Error('Failed to extract data', { cause: err })
@@ -642,4 +575,73 @@ export class VaultWorker {
                        throw new Error('Failed to load wallet', { cause: err })
                }
        }
+
+       // Action for selecting method execution
+       #parseAction (data: { [key: string]: unknown }) {
+               if (data.action == null) {
+                       throw new TypeError('Wallet action is required')
+               }
+               if (data.action !== 'STOP'
+                       && data.action !== 'create'
+                       && data.action !== 'derive'
+                       && data.action !== 'load'
+                       && data.action !== 'lock'
+                       && data.action !== 'sign'
+                       && data.action !== 'unlock'
+                       && data.action !== 'update'
+                       && data.action !== 'verify') {
+                       throw new TypeError('Invalid wallet action')
+               }
+               return data.action
+       }
+
+       // Worker message data itself
+       #parseData (data: unknown) {
+               if (data == null) {
+                       throw new TypeError('Worker received no data')
+               }
+               if (typeof data !== 'object') {
+                       throw new Error('Invalid data')
+               }
+               return data as { [key: string]: unknown }
+       }
+
+       // Salt created to derive CryptoKey from password; subsequently required to
+       // derive the same key for unlock requests
+       #parseKeySalt (action: string, data: { [key: string]: unknown }): ArrayBuffer {
+               if (action === 'unlock') {
+                       if (data.keySalt instanceof ArrayBuffer) {
+                               return data.keySalt
+                       } else {
+                               throw new TypeError('Key salt required to unlock wallet')
+                       }
+               } else {
+                       return crypto.getRandomValues(new Uint8Array(32)).buffer
+               }
+       }
+
+       // Initialization vector created to encrypt and lock the vault; subsequently
+       // required to decrypt and unlock the vault
+       #parseIv (action: string, data: { [key: string]: unknown }) {
+               if (action === 'unlock') {
+                       if (!(data.iv instanceof ArrayBuffer)) {
+                               throw new TypeError('Initialization vector required to unlock wallet')
+                       }
+               } else if (data.iv !== undefined) {
+                       throw new Error('IV is not allowed for this action', { cause: action })
+               }
+               return data.iv
+       }
+
+       // Algorithm used for wallet functions
+       #parseType (action: string, data: { [key: string]: unknown }) {
+               if (['create', 'load', 'unlock'].includes(action)) {
+                       if (data.type !== 'BIP-44' && data.type !== 'BLAKE2b') {
+                               throw new TypeError(`Type is required to ${action} wallet`)
+                       }
+               } else if (data.type !== undefined) {
+                       throw new Error('Type is not allowed for this action', { cause: action })
+               }
+               return data.type
+       }
 }
index 918cdd8673321db28ee5dfdb92d9dfaee78aa42f..234d850888be275cb673f86053a70a3854904a7c 100644 (file)
@@ -30,7 +30,7 @@ export async function _sign (wallet: Wallet, vault: Vault, index: unknown, block
                        const { signature } = await vault.request<ArrayBuffer>({
                                action: 'sign',
                                index,
-                               data: hex.toBuffer(block.hash)
+                               message: hex.toBuffer(block.hash)
                        })
                        block.signature = bytes.toHex(new Uint8Array(signature))
                }
index 4e30de505c7334e3408607154c6453253566d5bf..50d3f92556ff9f1b699d58d24e9794b35b4c8fa7 100644 (file)
@@ -21,12 +21,9 @@ export async function _update (wallet: Wallet, vault: Vault, password: unknown):
                        if (typeof password !== 'string') {
                                throw new TypeError('Password must be a string')
                        }
-                       const { encrypted } = await _get(wallet.id)
                        const response = await vault.request<ArrayBuffer>({
                                action: 'update',
-                               type: wallet.type,
-                               password: utf8.toBuffer(password),
-                               encrypted
+                               password: utf8.toBuffer(password)
                        })
                        password = undefined
                        record.iv = response.iv
index cda35856c51a3ad96b9ab8ca43a83e6822f61673..20ae465eff8e89e04303635e58180586db7e8878 100644 (file)
@@ -19,7 +19,7 @@ let Block
 */\r
 let Rpc\r
 /**\r
-* @type {import('../dist/types.d.ts').Tools}\r
+* @type {typeof import('../dist/types.d.ts').Tools}\r
 */\r
 let Tools\r
 /**\r