//! SPDX-License-Identifier: GPL-3.0-or-later
import * as nano25519 from 'nano25519'
+import { parentPort } from 'node:worker_threads'
import { BIP44_COIN_NANO } from '../constants'
import { Bip39, Bip44, Blake2b, WalletAesGcm } from '../crypto'
import { WalletType } from '../wallet'
import { VaultTimer } from './vault-timer'
/**
-* Cross-platform worker for managing wallet secrets.
-*/
-export class VaultWorker {
- #encoder: TextEncoder = new TextEncoder()
- #locked: boolean
- #timeout: number
- #timer: VaultTimer
- #type?: 'BIP-44' | 'BLAKE2b' | 'Exodus'
- #seed?: ArrayBuffer
- #mnemonic?: ArrayBuffer
- #parentPort?: any
-
- constructor () {
- this.#locked = true
- this.#timeout = 120_000
- this.#timer = new VaultTimer(() => { }, 0)
- this.#type = undefined
- this.#seed = undefined
- this.#mnemonic = undefined
-
- const listener = (event: MessageEvent<any>): void => {
- NODE: if (this.#parentPort == null) setTimeout(() => listener(event), 0)
- const data = this.#parseData(event.data)
- const action = this.#parseAction(data)
- const keySalt = this.#parseKeySalt(action, data)
- Passkey.create(action, keySalt, data)
- .then((key: CryptoKey | undefined): Promise<Record<string, boolean | number | ArrayBuffer> | void> => {
- const type = this.#parseType(action, data)
- const iv = this.#parseIv(action, data)
- const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = this.#extractData(action, data)
- switch (action) {
- case 'STOP': {
- BROWSER: close()
- NODE: process.exit()
- }
- case 'config': {
- return this.config(timeout)
- }
- case 'create': {
- return this.create(type, key, keySalt, mnemonicSalt)
- }
- case 'derive': {
- return this.derive(index)
- }
- case 'load': {
- return this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
- }
- case 'lock': {
- return this.lock()
- }
- case 'sign': {
- return this.sign(index, message)
- }
- case 'unlock': {
- return this.unlock(type, key, iv, encrypted)
- }
- case 'update': {
- return this.update(key, keySalt)
- }
- case 'verify': {
- return Promise.resolve(this.verify(seed, mnemonicPhrase))
- }
- default: {
- throw new Error(`Unknown wallet action '${action}'`)
- }
- }
- })
- .then((result: Record<string, boolean | number | ArrayBuffer> | void) => {
- const transfer = []
- if (result) {
- for (const r of Object.values(result)) {
- if (r instanceof ArrayBuffer) {
- transfer.push(r)
- }
- }
- }
- //@ts-expect-error
- BROWSER: postMessage(result, transfer)
- NODE: this.#parentPort?.postMessage(result, transfer)
- })
- .catch((err: any) => {
- console.error(err)
- for (let data of Object.values(event.data)) {
- if (data instanceof ArrayBuffer && !data.detached) {
- new Uint8Array(data).fill(0).buffer.transfer?.()
- }
- data = undefined
- }
- BROWSER: postMessage({ error: 'Failed to process Vault request', cause: err })
- NODE: this.#parentPort?.postMessage({ error: 'Failed to process Vault request', cause: err })
- })
- }
- BROWSER: addEventListener('message', listener)
- NODE: {
- if (this.#parentPort == null) {
- import('node:worker_threads')
- .then(({ parentPort }) => {
- this.#parentPort = parentPort
- this.#parentPort.on('message', listener)
- })
- }
- }
- }
+ * Cross-platform worker for managing wallet secrets.
+ */
+const _encoder: TextEncoder = new TextEncoder()
+let _locked: boolean = true
+let _timeout: number = 120_000
+let _timer: VaultTimer = new VaultTimer(() => { }, 0)
+let _type: 'BIP-44' | 'BLAKE2b' | 'Exodus' | undefined = undefined
+let _seed: ArrayBuffer | undefined = undefined
+let _mnemonic: ArrayBuffer | undefined = undefined
- /**
- * Configures vault settings. The wallet must be unlocked prior to
- * configuration.
- */
- config (timeout?: number): Promise<void> {
- try {
- this.#timer?.pause()
- if (this.#locked) {
- throw new Error('Wallet is locked')
- }
- if (typeof timeout === 'number') {
- if (timeout < 10) {
- throw new RangeError('Timeout must be at least 10 seconds')
+const listener = (event: MessageEvent<any>): void => {
+ const { url, id } = event.data
+ if (url !== location.href) return
+ NODE: if (parentPort == null) setTimeout(() => listener(event), 0)
+ const data = _parseData(event.data)
+ const action = _parseAction(data)
+ const keySalt = _parseKeySalt(action, data)
+ Passkey.create(action, keySalt, data)
+ .then((key: CryptoKey | undefined): Promise<Record<string, boolean | number | ArrayBuffer> | void> => {
+ const type = _parseType(action, data)
+ const iv = _parseIv(action, data)
+ const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = _extractData(action, data)
+ switch (action) {
+ case 'STOP': {
+ BROWSER: close()
+ NODE: process.exit()
+ }
+ case 'config': {
+ return config(timeout)
+ }
+ case 'create': {
+ return create(type, key, keySalt, mnemonicSalt)
+ }
+ case 'derive': {
+ return derive(index)
+ }
+ case 'load': {
+ return load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+ }
+ case 'lock': {
+ return lock()
}
- if (timeout > 600) {
- throw new RangeError('Timeout must be at most 10 minutes')
+ case 'sign': {
+ return sign(index, message)
+ }
+ case 'unlock': {
+ return unlock(type, key, iv, encrypted)
+ }
+ case 'update': {
+ return update(key, keySalt)
+ }
+ case 'verify': {
+ return Promise.resolve(verify(seed, mnemonicPhrase))
+ }
+ default: {
+ throw new Error(`Unknown wallet action '${action}'`)
}
- this.#timeout = timeout * 1000
- this.#timer = new VaultTimer(() => this.lock(), this.#timeout)
}
- return Promise.resolve()
- } catch (err) {
- console.error(err)
- this.#timer?.resume()
- throw new Error('Failed to configure Vault', { cause: err })
- }
- }
-
- /**
- * Generates a new mnemonic and seed and then returns the initialization vector
- * vector, salt, and encrypted data representing the wallet in a locked state.
- */
- create (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
- if (type !== 'BIP-44' && type !== 'BLAKE2b') {
- throw new TypeError('Unsupported software wallet algorithm', { cause: type })
- }
- try {
- const entropy = crypto.getRandomValues(new Uint8Array(32))
- return Bip39.fromEntropy(entropy)
- .then(bip39 => this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt))
- .then(({ iv, salt, encrypted }) => {
- entropy.fill(0)
- if (this.#seed == null || this.#mnemonic == null) {
- throw new Error('Failed to generate seed and mnemonic')
+ })
+ .then((result: Record<string, boolean | number | ArrayBuffer> | void) => {
+ result ??= {}
+ const transfer: ArrayBuffer[] = []
+ if (result) {
+ for (const r of Object.values(result)) {
+ if (r instanceof ArrayBuffer) {
+ // transfer.push(r)
}
- return { iv, salt, encrypted, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
- })
- } catch (err) {
- console.error(err)
- throw new Error('Failed to create wallet', { cause: err })
- } finally {
- this.lock()
- }
- }
-
- /**
- * Derives the private and public keys of a child account from the current
- * wallet seed at a specified index and then returns the public key. The wallet
- * must be unlocked prior to derivation.
- */
- derive (index?: number): Promise<Record<string, number | ArrayBuffer>> {
- try {
- this.#timer.pause()
- if (this.#locked) {
- throw new Error('Wallet is locked')
+ }
}
- if (this.#seed == null) {
- throw new Error('Wallet seed not found')
+ result.url = url
+ result.id = id
+ //@ts-expect-error
+ BROWSER: postMessage(result, transfer)
+ NODE: parentPort?.postMessage(result, transfer)
+ })
+ .catch((err: any) => {
+ for (let data of Object.values(event.data)) {
+ if (data instanceof ArrayBuffer && !data.detached) {
+ new Uint8Array(data).fill(0).buffer.transfer?.()
+ }
+ data = undefined
}
- if (this.#type !== 'BIP-44' && this.#type !== 'BLAKE2b' && this.#type !== 'Exodus') {
- throw new Error('Invalid wallet type')
+ 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: parentPort?.on('message', listener)
+
+/**
+ * Configures vault settings. The wallet must be unlocked prior to
+ * configuration.
+ */
+async function config (timeout?: number): Promise<void> {
+ try {
+ _timer?.pause()
+ if (_locked) {
+ throw new Error('Wallet is locked')
+ }
+ if (typeof timeout === 'number') {
+ if (timeout < 10) {
+ throw new RangeError('Timeout must be at least 10 seconds')
}
- if (typeof index !== 'number') {
- throw new Error('Invalid wallet account index')
+ if (timeout > 600) {
+ throw new RangeError('Timeout must be at most 10 minutes')
}
- const derive = this.#type === 'BIP-44'
- ? Bip44.ckd('ed25519 seed', this.#seed, BIP44_COIN_NANO, index)
- : this.#type === 'Exodus'
- ? Bip44.ckd('Bitcoin seed', this.#seed, 0x100, index, 0, 0)
- : Blake2b.ckd(this.#seed, index)
- return derive.then(result => {
- const prv = new Uint8Array(result)
- const pub = nano25519.derive(prv)
- this.#timer = new VaultTimer(() => this.lock(), this.#timeout)
- return { index, publicKey: pub.buffer }
- })
- } catch (err) {
- console.error(err)
- this.#timer.resume()
- throw new Error('Failed to derive account', { cause: err })
+ timeout = timeout * 1000
+ _timer = new VaultTimer(() => lock(), timeout)
}
+ return Promise.resolve()
+ } catch (err) {
+ console.error(err)
+ _timer?.resume()
+ throw new Error('Failed to configure Vault', { cause: err })
}
+}
- /**
- * Encrypts an existing seed or mnemonic+salt and returns the initialization
- * vector, salt, and encrypted data representing the wallet in a locked state.
- */
- load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
- if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
- throw new TypeError('Unsupported software wallet algorithm', { cause: type })
- }
- return this.#load(type, key, keySalt, secret, mnemonicSalt)
- .then(record => {
- if (this.#seed == null) {
- throw new Error('Wallet seed not found')
+/**
+* Generates a new mnemonic and seed and then returns the initialization vector
+* vector, salt, and encrypted data representing the wallet in a locked state.
+*/
+async function create (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+ if (type !== 'BIP-44' && type !== 'BLAKE2b') {
+ throw new TypeError('Unsupported software wallet algorithm', { cause: type })
+ }
+ try {
+ const entropy = crypto.getRandomValues(new Uint8Array(32))
+ return Bip39.fromEntropy(entropy)
+ .then(bip39 => _load(type, key, keySalt, bip39.phrase, mnemonicSalt))
+ .then(({ iv, salt, encrypted }) => {
+ entropy.fill(0)
+ if (_seed == null || _mnemonic == null) {
+ throw new Error('Failed to generate seed and mnemonic')
}
- return record
- })
- .catch(err => {
- console.error(err)
- throw new Error('Failed to load wallet', { cause: err })
+ return { iv, salt, encrypted, seed: _seed.slice(), mnemonic: _mnemonic.slice() }
})
- .finally(() => this.lock())
+ } catch (err) {
+ console.error(err)
+ throw new Error('Failed to create wallet', { cause: err })
+ } finally {
+ lock()
}
+}
- lock (): Promise<void> {
- this.#mnemonic = undefined
- this.#seed = undefined
- this.#locked = true
- this.#timer?.pause()
- BROWSER: postMessage('locked')
- NODE: this.#parentPort?.postMessage('locked')
- return Promise.resolve()
+/**
+* Derives the private and public keys of a child account from the current
+* wallet seed at a specified index and then returns the public key. The wallet
+* must be unlocked prior to derivation.
+*/
+async function derive (index?: number): Promise<Record<string, number | ArrayBuffer>> {
+ try {
+ _timer.pause()
+ if (_locked) {
+ throw new Error('Wallet is locked')
+ }
+ if (_seed == null) {
+ throw new Error('Wallet seed not found')
+ }
+ if (_type !== 'BIP-44' && _type !== 'BLAKE2b' && _type !== 'Exodus') {
+ throw new Error('Invalid wallet type')
+ }
+ if (typeof index !== 'number') {
+ throw new Error('Invalid wallet account index')
+ }
+ const derive = _type === 'BIP-44'
+ ? Bip44.ckd('ed25519 seed', _seed, BIP44_COIN_NANO, index)
+ : _type === 'Exodus'
+ ? Bip44.ckd('Bitcoin seed', _seed, 0x100, index, 0, 0)
+ : Blake2b.ckd(_seed, index)
+ return derive.then(result => {
+ const prv = new Uint8Array(result)
+ const pub = nano25519.derive(prv)
+ _timer = new VaultTimer(() => lock(), _timeout)
+ return { index, publicKey: pub.buffer }
+ })
+ } catch (err) {
+ console.error(err)
+ _timer.resume()
+ throw new Error('Failed to derive account', { cause: err })
}
+}
- /**
- * 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.
- */
- sign (index?: number, data?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
- try {
- this.#timer.pause()
- if (this.#locked) {
- throw new Error('Wallet is locked')
- }
- if (this.#seed == null) {
+/**
+* Encrypts an existing seed or mnemonic+salt and returns the initialization
+* vector, salt, and encrypted data representing the wallet in a locked state.
+*/
+async function load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+ if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
+ throw new TypeError('Unsupported software wallet algorithm', { cause: type })
+ }
+ return _load(type, key, keySalt, secret, mnemonicSalt)
+ .then(record => {
+ if (_seed == null) {
throw new Error('Wallet seed not found')
}
- if (index == null) {
- throw new Error('Wallet account index is required to sign')
- }
- if (data == null) {
- throw new Error('Data to sign not found')
- }
- const derive = this.#type === 'BLAKE2b'
- ? Blake2b.ckd(this.#seed, index)
- : Bip44.ckd(this.#type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', this.#seed, BIP44_COIN_NANO, index)
- return derive.then(result => {
- const prv = new Uint8Array(result)
- const pub = nano25519.derive(prv)
- const sig = nano25519.sign(new Uint8Array(data), new Uint8Array([...prv, ...pub]))
- this.#timer = new VaultTimer(() => this.lock(), this.#timeout)
- return { signature: sig.buffer }
- })
- } catch (err) {
+ return record
+ })
+ .catch(err => {
console.error(err)
- this.#timer.resume()
- throw new Error('Failed to sign message', { cause: err })
+ throw new Error('Failed to load wallet', { cause: err })
+ })
+ .finally(() => lock())
+}
+
+async function lock (): Promise<void> {
+ _mnemonic = undefined
+ _seed = undefined
+ _locked = true
+ _timer?.pause()
+ BROWSER: postMessage('locked')
+ NODE: parentPort?.postMessage('locked')
+ return Promise.resolve()
+}
+
+/**
+* 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.
+*/
+async function sign (index?: number, data?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
+ try {
+ _timer.pause()
+ if (_locked) {
+ throw new Error('Wallet is locked')
+ }
+ if (_seed == null) {
+ throw new Error('Wallet seed not found')
+ }
+ if (index == null) {
+ throw new Error('Wallet account index is required to sign')
+ }
+ if (data == null) {
+ throw new Error('Data to sign not found')
}
+ const derive = _type === 'BLAKE2b'
+ ? Blake2b.ckd(_seed, index)
+ : Bip44.ckd(_type === 'Exodus' ? 'Bitcoin seed' : 'ed25519 seed', _seed, BIP44_COIN_NANO, index)
+ return derive.then(result => {
+ const prv = new Uint8Array(result)
+ const pub = nano25519.derive(prv)
+ const sig = nano25519.sign(new Uint8Array(data), new Uint8Array([...prv, ...pub]))
+ _timer = new VaultTimer(() => lock(), _timeout)
+ return { signature: sig.buffer }
+ })
+ } catch (err) {
+ console.error(err)
+ _timer.resume()
+ throw new Error('Failed to sign message', { cause: err })
}
+}
- /**
- * Decrypts the input and sets the seed and, if it is included, the mnemonic.
- */
- unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<void> {
- if (type == null) {
- throw new TypeError('Wallet type is required')
- }
- if (type === 'Ledger') {
- this.#locked = false
+/**
+* Decrypts the input and sets the seed and, if it is included, the mnemonic.
+*/
+async function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<void> {
+ if (type == null) {
+ throw new TypeError('Wallet type is required')
+ }
+ if (type === 'Ledger') {
+ _locked = false
+ BROWSER: postMessage('unlocked')
+ NODE: parentPort?.postMessage('unlocked')
+ return Promise.resolve()
+ }
+ 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')
+ }
+ _timer?.pause()
+ return WalletAesGcm.decrypt(type, key, iv, encrypted)
+ .then(({ mnemonic, seed }) => {
+ if (!(seed instanceof ArrayBuffer)) {
+ throw new TypeError('Invalid seed')
+ }
+ if (mnemonic != null && !(mnemonic instanceof ArrayBuffer)) {
+ throw new TypeError('Invalid mnemonic')
+ }
+ type = type
+ seed = seed
+ mnemonic = mnemonic
+ _locked = false
+ _timer = new VaultTimer(lock, _timeout)
BROWSER: postMessage('unlocked')
- NODE: this.#parentPort?.postMessage('unlocked')
- return Promise.resolve()
+ NODE: parentPort?.postMessage('unlocked')
+ })
+ .catch(err => {
+ console.error(err)
+ _timer?.resume()
+ throw new Error('Failed to unlock wallet', { cause: err })
+ })
+}
+
+/**
+* Re-encrypts the wallet with a new password.
+*/
+async function update (key?: CryptoKey, salt?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
+ try {
+ _timer.pause()
+ if (_locked) {
+ throw new Error('Wallet is locked')
}
- if (key == null) {
- throw new TypeError('Wallet password is required')
+ if (_seed == null) {
+ throw new Error('Wallet seed not found')
}
- if (iv == null) {
- throw new TypeError('Wallet IV is required')
+ if (_type == null) {
+ throw new Error('Wallet type not found')
}
- if (encrypted == null) {
- throw new TypeError('Wallet encrypted data is required')
+ if (key == null || salt == null) {
+ throw new TypeError('Wallet password is required')
}
- this.#timer?.pause()
- return WalletAesGcm.decrypt(type, key, iv, encrypted)
- .then(({ mnemonic, seed }) => {
- if (!(seed instanceof ArrayBuffer)) {
- throw new TypeError('Invalid seed')
- }
- if (mnemonic != null && !(mnemonic instanceof ArrayBuffer)) {
- throw new TypeError('Invalid mnemonic')
- }
- this.#type = type
- this.#seed = seed
- this.#mnemonic = mnemonic
- this.#locked = false
- this.#timer = new VaultTimer(this.lock.bind(this), this.#timeout)
- BROWSER: postMessage('unlocked')
- NODE: this.#parentPort?.postMessage('unlocked')
- })
- .catch(err => {
- console.error(err)
- this.#timer?.resume()
- throw new Error('Failed to unlock wallet', { cause: err })
+ return WalletAesGcm.encrypt(_type, key, _seed, _mnemonic)
+ .then(({ iv, encrypted }) => {
+ _timer = new VaultTimer(() => lock(), _timeout)
+ return { iv, salt, encrypted }
})
+ } catch (err) {
+ console.error(err)
+ _timer.resume()
+ throw new Error('Failed to update wallet password', { cause: err })
}
+}
- /**
- * Re-encrypts the wallet with a new password.
- */
- update (key?: CryptoKey, salt?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
- try {
- this.#timer.pause()
- if (this.#locked) {
- throw new Error('Wallet is locked')
- }
- if (this.#seed == null) {
- throw new Error('Wallet seed not found')
- }
- if (this.#type == null) {
- throw new Error('Wallet type not found')
- }
- if (key == null || salt == null) {
- throw new TypeError('Wallet password is required')
- }
- return WalletAesGcm.encrypt(this.#type, key, this.#seed, this.#mnemonic)
- .then(({ iv, encrypted }) => {
- this.#timer = new VaultTimer(() => this.lock(), this.#timeout)
- return { iv, salt, encrypted }
- })
- } catch (err) {
- console.error(err)
- this.#timer.resume()
- throw new Error('Failed to update wallet password', { cause: err })
+/**
+* Checks the seed and, if it exists, the mnemonic against input. The wallet
+* must be unlocked prior to verification.
+*/
+function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record<string, boolean> {
+ try {
+ if (_locked) {
+ throw new Error('Wallet is locked')
}
- }
-
- /**
- * Checks the seed and, if it exists, the mnemonic against input. The wallet
- * must be unlocked prior to verification.
- */
- verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record<string, boolean> {
- try {
- if (this.#locked) {
- throw new Error('Wallet is locked')
- }
- if (this.#seed == null) {
- throw new Error('Wallet seed not found')
- }
- if (seed == null && mnemonicPhrase == null) {
- throw new Error('Seed or mnemonic phrase is required')
- }
- if (seed != null && mnemonicPhrase != null) {
- throw new Error('Seed or mnemonic phrase must be verified separately')
- }
- let isVerified = false
- if (seed != null) {
- let diff = 0
- const userSeed = new Uint8Array(seed)
- const thisSeed = new Uint8Array(this.#seed)
- for (let i = 0; i < userSeed.byteLength; i++) {
- diff |= userSeed[i] ^ thisSeed[i]
- }
- isVerified = diff === 0
- }
- if (mnemonicPhrase != null) {
- console.log(new TextDecoder().decode((new Uint8Array(this.#mnemonic ?? []))))
- let diff = 0
- const userMnemonic = this.#encoder.encode(mnemonicPhrase)
- const thisMnemonic = new Uint8Array(this.#mnemonic ?? [])
- for (let i = 0; i < userMnemonic.byteLength; i++) {
- diff |= userMnemonic[i] ^ thisMnemonic[i]
- }
- isVerified = diff === 0
- }
- return { isVerified }
- } catch (err) {
- console.error(err)
- throw new Error('Failed to verify wallet', { cause: err })
+ if (seed == null) {
+ throw new Error('Wallet seed not found')
}
+ if (seed == null && mnemonicPhrase == null) {
+ throw new Error('Seed or mnemonic phrase is required')
+ }
+ if (seed != null && mnemonicPhrase != null) {
+ throw new Error('Seed or mnemonic phrase must be verified separately')
+ }
+ let isVerified = false
+ if (seed != null) {
+ let diff = 0
+ const userSeed = new Uint8Array(seed)
+ const thisSeed = new Uint8Array(seed)
+ for (let i = 0; i < userSeed.byteLength; i++) {
+ diff |= userSeed[i] ^ thisSeed[i]
+ }
+ isVerified = diff === 0
+ }
+ if (mnemonicPhrase != null) {
+ console.log(new TextDecoder().decode((new Uint8Array(_mnemonic ?? []))))
+ let diff = 0
+ const userMnemonic = _encoder.encode(mnemonicPhrase)
+ const thisMnemonic = new Uint8Array(_mnemonic ?? [])
+ for (let i = 0; i < userMnemonic.byteLength; i++) {
+ diff |= userMnemonic[i] ^ thisMnemonic[i]
+ }
+ isVerified = diff === 0
+ }
+ return { isVerified }
+ } catch (err) {
+ console.error(err)
+ throw new Error('Failed to verify wallet', { cause: err })
}
+}
- /**
- * Parse inbound message from main thread into typechecked variables.
- */
- #extractData (action: string, data: { [key: string]: unknown }) {
- try {
- // 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')
- }
+/**
+* Parse inbound message from main thread into typechecked variables.
+*/
+function _extractData (action: string, data: { [key: string]: unknown }) {
+ try {
+ // 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')
+ }
- // Seed to load
- if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) {
- throw new TypeError('Seed required to load 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
- }
+ // Seed to load
+ if (action === 'load' && 'seed' in data && !(data.seed instanceof ArrayBuffer)) {
+ throw new TypeError('Seed required to load 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
+ }
- // 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
+ // 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
- // 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
+ // 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
- // 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')
- }
+ // Encrypted seed and possibly mnemonic
+ if (action === 'unlock') {
+ if (data.encrypted == null) {
+ throw new TypeError('Wallet encrypted secrets not found')
}
- const encrypted = data.encrypted instanceof ArrayBuffer
- ? data.encrypted.slice()
- : undefined
- if (data.encrypted instanceof ArrayBuffer) {
- new Uint8Array(data.encrypted).fill(0)
- delete data.encrypted
+ 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
+ }
- // 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 data.index !== 'number') {
+ throw new TypeError('Index is required to derive an account private key')
+ }
+ const index = typeof data.index === 'number'
+ ? data.index
+ : undefined
- // 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')
- }
+ // Data to sign
+ if (action === 'sign') {
+ if (data.message == null) {
+ throw new TypeError('Data to sign not found')
}
- const message = data.message instanceof ArrayBuffer
- ? data.message
- : undefined
- delete data.message
-
- // Vault configuration
- if (action === 'config') {
- if (data.timeout == null) {
- throw new TypeError('Configuration not found')
- }
- if (typeof data.timeout !== 'number') {
- throw new TypeError('Invalid timeout configuration')
- }
+ if (!(data.message instanceof ArrayBuffer)) {
+ throw new TypeError('Invalid data to sign')
}
- const timeout = typeof data.timeout === 'number'
- ? data.timeout
- : undefined
-
- return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message, timeout }
- } catch (err) {
- console.error(err)
- throw new Error('Failed to extract data', { cause: err })
}
- }
+ const message = data.message instanceof ArrayBuffer
+ ? data.message
+ : undefined
+ delete data.message
- /**
- * Encrypts an existing seed or mnemonic+salt and returns the initialization
- * vector, salt, and encrypted data representing the wallet in a locked state.
- */
- #load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
- try {
- if (!this.#locked) {
- throw new Error('Wallet is in use')
- }
- if (key == null || keySalt == null) {
- throw new Error('Wallet password is required')
+ // Vault configuration
+ if (action === 'config') {
+ if (data.timeout == null) {
+ throw new TypeError('Configuration not found')
}
- if (type == null) {
- throw new TypeError('Wallet type is required')
+ if (typeof data.timeout !== 'number') {
+ throw new TypeError('Invalid timeout configuration')
}
- if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
- throw new TypeError('Invalid wallet type')
+ }
+ const timeout = typeof data.timeout === 'number'
+ ? data.timeout
+ : undefined
+
+ return { seed, mnemonicPhrase, mnemonicSalt, encrypted, index, message, timeout }
+ } catch (err) {
+ console.error(err)
+ throw new Error('Failed to extract data', { cause: err })
+ }
+}
+
+/**
+* Encrypts an existing seed or mnemonic+salt and returns the initialization
+* vector, salt, and encrypted data representing the wallet in a locked state.
+*/
+function _load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+ try {
+ if (!_locked) {
+ throw new Error('Wallet is in use')
+ }
+ if (key == null || keySalt == null) {
+ throw new Error('Wallet password is required')
+ }
+ if (type == null) {
+ throw new TypeError('Wallet type is required')
+ }
+ if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
+ throw new TypeError('Invalid wallet type')
+ }
+ if (secret == null) {
+ throw new TypeError('Seed or mnemonic is required')
+ }
+ if (typeof secret !== 'string' && mnemonicSalt !== undefined) {
+ throw new TypeError('Mnemonic must be a string')
+ }
+ if (type === 'BIP-44') {
+ if (secret instanceof ArrayBuffer && (secret.byteLength < 16 || secret.byteLength > 64)) {
+ throw new RangeError('Seed for BIP-44 wallet must be 16-64 bytes')
}
- if (secret == null) {
- throw new TypeError('Seed or mnemonic is required')
+ }
+ if (type === 'BLAKE2b') {
+ if (secret instanceof ArrayBuffer && secret.byteLength !== 32) {
+ throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes')
}
- if (typeof secret !== 'string' && mnemonicSalt !== undefined) {
- throw new TypeError('Mnemonic must be a string')
+ }
+ if (type === 'Exodus') {
+ if (typeof secret === 'string' && secret.split(' ').length !== 12) {
+ throw new RangeError('Mnemonic for Exodus wallet must be 12 words')
}
- if (type === 'BIP-44') {
- if (secret instanceof ArrayBuffer && (secret.byteLength < 16 || secret.byteLength > 64)) {
- throw new RangeError('Seed for BIP-44 wallet must be 16-64 bytes')
- }
+ if (secret instanceof ArrayBuffer && secret.byteLength !== 64) {
+ throw new RangeError('Seed for Exodus wallet must be 64 bytes')
}
+ }
+ _type = type
+ let seed: Promise<ArrayBuffer>
+ if (secret instanceof ArrayBuffer) {
if (type === 'BLAKE2b') {
- if (secret instanceof ArrayBuffer && secret.byteLength !== 32) {
- throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes')
- }
- }
- if (type === 'Exodus') {
- if (typeof secret === 'string' && secret.split(' ').length !== 12) {
- throw new RangeError('Mnemonic for Exodus wallet must be 12 words')
- }
- if (secret instanceof ArrayBuffer && secret.byteLength !== 64) {
- throw new RangeError('Seed for Exodus wallet must be 64 bytes')
- }
- }
- this.#type = type
- let seed: Promise<ArrayBuffer>
- if (secret instanceof ArrayBuffer) {
- if (type === 'BLAKE2b') {
- seed = Bip39.fromEntropy(new Uint8Array(secret))
- .then(bip39 => {
- this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer
- return secret
- })
- } else {
- seed = Promise.resolve(secret)
- }
- } else {
- seed = Bip39.fromPhrase(secret)
+ seed = Bip39.fromEntropy(new Uint8Array(secret))
.then(bip39 => {
- this.#mnemonic = new Uint8Array(this.#encoder.encode(bip39.phrase ?? '')).buffer
- const derive = type === 'BLAKE2b'
- ? Promise.resolve(bip39.toBlake2bSeed())
- : bip39.toBip39Seed(mnemonicSalt ?? '')
- return derive.then(s => s.buffer)
+ _mnemonic = new Uint8Array(_encoder.encode(bip39.phrase ?? '')).buffer
+ return secret
})
+ } else {
+ seed = Promise.resolve(secret)
}
- return seed.then(seed => {
- this.#seed = seed
- return WalletAesGcm.encrypt(type, key, this.#seed, this.#mnemonic)
- .then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted }))
- })
- } catch (err) {
- throw new Error('Failed to load wallet', { cause: err })
+ } else {
+ seed = Bip39.fromPhrase(secret)
+ .then(bip39 => {
+ _mnemonic = new Uint8Array(_encoder.encode(bip39.phrase ?? '')).buffer
+ const derive = type === 'BLAKE2b'
+ ? Promise.resolve(bip39.toBlake2bSeed())
+ : bip39.toBip39Seed(mnemonicSalt ?? '')
+ return derive.then(s => s.buffer)
+ })
}
+ return seed.then(seed => {
+ _seed = seed
+ return WalletAesGcm.encrypt(type, key, _seed, _mnemonic)
+ .then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted }))
+ })
+ } catch (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 !== 'config'
- && 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
+// Action for selecting method execution
+function _parseAction (data: { [key: string]: unknown }) {
+ if (data.action == null) {
+ throw new TypeError('Wallet action is required')
}
+ if (data.action !== 'STOP'
+ && data.action !== 'config'
+ && 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 }
+// Worker message data itself
+function _parseData (data: unknown): Record<string, unknown> {
+ if (data == null) {
+ throw new TypeError('Worker received no data')
+ }
+ if (typeof data !== 'object') {
+ throw new Error('Invalid data')
}
+ return data as Record<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')
- }
+// Salt created to derive CryptoKey from password; subsequently required to
+// derive the same key for unlock requests
+function _parseKeySalt (action: string, data: { [key: string]: unknown }): ArrayBuffer {
+ if (action === 'unlock') {
+ if (data.keySalt instanceof ArrayBuffer) {
+ return data.keySalt
} else {
- return crypto.getRandomValues(new Uint8Array(32)).buffer
+ 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 })
+// Initialization vector created to encrypt and lock the vault; subsequently
+// required to decrypt and unlock the vault
+function _parseIv (action: string, data: { [key: string]: unknown }) {
+ if (action === 'unlock') {
+ if (!(data.iv instanceof ArrayBuffer)) {
+ throw new TypeError('Initialization vector required to unlock wallet')
}
- return data.iv
+ } 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' && data.type !== 'Exodus' && data.type !== 'Ledger') {
- 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 })
+// Algorithm used for wallet functions
+function _parseType (action: string, data: { [key: string]: unknown }) {
+ if (['create', 'load', 'unlock'].includes(action)) {
+ if (data.type !== 'BIP-44' && data.type !== 'BLAKE2b' && data.type !== 'Exodus' && data.type !== 'Ledger') {
+ throw new TypeError(`Type is required to ${action} wallet`)
}
- return data.type
+ } else if (data.type !== undefined) {
+ throw new Error('Type is not allowed for this action', { cause: action })
}
+ return data.type
}
-const v = new VaultWorker()