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 })
}
}
- #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> {
/**
* 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 })
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
+ }
}