* Cross-platform worker for managing wallet secrets.
*/
export class VaultWorker {
- static locked: boolean
- static timeout: VaultTimer
- static type?: 'BIP-44' | 'BLAKE2b'
- static seed?: ArrayBuffer
- static mnemonic?: ArrayBuffer
- static parentPort?: any
+ #locked: boolean
+ #timeout: VaultTimer
+ #type?: 'BIP-44' | 'BLAKE2b'
+ #seed?: ArrayBuffer
+ #mnemonic?: ArrayBuffer
+ #parentPort?: any
- static init (): void {
- this.locked = true
- this.timeout = new VaultTimer(() => { }, 0)
- this.type = undefined
- this.seed = undefined
- this.mnemonic = undefined
- NODE: this.parentPort = parentPort
+ constructor () {
+ this.#locked = true
+ this.#timeout = new VaultTimer(() => { }, 0)
+ this.#type = undefined
+ this.#seed = undefined
+ this.#mnemonic = undefined
+ NODE: this.#parentPort = parentPort
const listener = (message: MessageEvent<any>): Promise<void> => {
- return this._extractData(message.data)
+ return this.#extractData(message.data)
.then(extracted => {
const {
action,
break
}
case 'load': {
- result = this.load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+ result = this.#load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
break
}
case 'lock': {
})
}
BROWSER: addEventListener('message', listener)
- NODE: this.parentPort?.on('message', listener)
+ NODE: this.#parentPort?.on('message', listener)
}
/**
* 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 create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+ create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
try {
const entropy = crypto.getRandomValues(new Uint8Array(32))
return Bip39.fromEntropy(entropy)
- .then(bip39 => this._load(type, key, keySalt, bip39.phrase, mnemonicSalt))
+ .then(bip39 => this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt))
.then(({ iv, salt, encrypted }) => {
- if (this.seed == null || this.mnemonic == null) {
+ if (this.#seed == null || this.#mnemonic == null) {
throw new Error('Failed to generate seed and mnemonic')
}
- return { iv, salt, encrypted, seed: this.seed.slice(), mnemonic: this.mnemonic.slice() }
+ return { iv, salt, encrypted, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
})
} catch (err) {
console.error(err)
* wallet seed at a specified index and then returns the public key. The wallet
* must be unlocked prior to derivation.
*/
- static derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
+ derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
try {
- this.timeout.pause()
- if (this.locked) {
+ this.#timeout.pause()
+ if (this.#locked) {
throw new Error('Wallet is locked')
}
- if (this.seed == null) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
- if (this.type !== 'BIP-44' && this.type !== 'BLAKE2b') {
+ if (this.#type !== 'BIP-44' && this.#type !== 'BLAKE2b') {
throw new Error('Invalid wallet type')
}
if (typeof index !== 'number') {
throw new Error('Invalid wallet account index')
}
- const derive = this.type === 'BIP-44'
- ? Bip44.ckd(this.seed, BIP44_COIN_NANO, index)
- : Promise.resolve(this._deriveBlake2bPrivateKey(this.seed, index))
+ 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)
+ this.#timeout = new VaultTimer(() => this.lock(), 120000)
return { index, publicKey: pub.buffer }
})
} catch (err) {
console.error(err)
- this.timeout.resume()
+ this.#timeout.resume()
throw new Error('Failed to derive account', { 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.
*/
- 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)
+ 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) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
return record
.finally(() => this.lock())
}
- static lock (): NamedData<boolean> {
- this.mnemonic = undefined
- this.seed = undefined
- this.locked = true
- this.timeout?.pause()
- return { isLocked: this.locked }
+ lock (): NamedData<boolean> {
+ this.#mnemonic = undefined
+ this.#seed = undefined
+ this.#locked = true
+ this.#timeout?.pause()
+ return { isLocked: this.#locked }
}
/**
* 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 sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
+ sign (index?: number, data?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
try {
- this.timeout.pause()
- if (this.locked) {
+ this.#timeout.pause()
+ if (this.#locked) {
throw new Error('Wallet is locked')
}
- if (this.seed == null) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
if (index == null) {
if (data == null) {
throw new Error('Data to sign not found')
}
- const derive = this.type === 'BIP-44'
- ? Bip44.ckd(this.seed, BIP44_COIN_NANO, index)
- : Promise.resolve(this._deriveBlake2bPrivateKey(this.seed, index))
+ 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)
+ this.#timeout = new VaultTimer(() => this.lock(), 120000)
return { signature: sig.buffer }
})
} catch (err) {
console.error(err)
- this.timeout.resume()
+ this.#timeout.resume()
throw new Error('Failed to sign message', { cause: err })
}
}
/**
* Decrypts the input and sets the seed and, if it is included, the mnemonic.
*/
- static unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
+ unlock (type?: string, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<NamedData<boolean>> {
if (type == null) {
throw new TypeError('Wallet type is required')
}
if (encrypted == null) {
throw new TypeError('Wallet encrypted data is required')
}
- this.timeout?.pause()
- return this._decryptWallet(type, key, iv, encrypted)
+ this.#timeout?.pause()
+ return this.#decryptWallet(type, key, iv, encrypted)
.then(() => {
- if (!(this.seed instanceof ArrayBuffer)) {
+ if (!(this.#seed instanceof ArrayBuffer)) {
throw new TypeError('Invalid seed')
}
- if (this.mnemonic != null && !(this.mnemonic instanceof ArrayBuffer)) {
+ 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 }
+ this.#locked = false
+ this.#timeout = new VaultTimer(() => this.lock(), 120000)
+ return { isUnlocked: !this.#locked }
})
.catch(err => {
console.error(err)
- this.timeout?.resume()
+ this.#timeout?.resume()
throw new Error('Failed to unlock wallet', { cause: err })
})
}
/**
* Re-encrypts the wallet with a new password.
*/
- static update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
+ update (key?: CryptoKey, keySalt?: ArrayBuffer): Promise<NamedData<ArrayBuffer>> {
try {
- this.timeout.pause()
- if (this.locked) {
+ this.#timeout.pause()
+ if (this.#locked) {
throw new Error('Wallet is locked')
}
- if (this.seed == null) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
if (key == null || keySalt == null) {
throw new TypeError('Wallet password is required')
}
- return this._encryptWallet(key)
+ return this.#encryptWallet(key)
.then(({ iv, encrypted }) => {
- this.timeout = new VaultTimer(() => this.lock(), 120000)
+ this.#timeout = new VaultTimer(() => this.lock(), 120000)
return { iv, salt: keySalt, encrypted }
})
} catch (err) {
console.error(err)
- this.timeout.resume()
+ this.#timeout.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.
*/
- static verify (seed?: ArrayBuffer, mnemonicPhrase?: string): NamedData<boolean> {
+ verify (seed?: ArrayBuffer, mnemonicPhrase?: string): NamedData<boolean> {
try {
- if (this.locked) {
+ if (this.#locked) {
throw new Error('Wallet is locked')
}
- if (this.seed == null) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
if (seed == null && mnemonicPhrase == null) {
if (seed != null) {
let diff = 0
const userSeed = new Uint8Array(seed)
- const thisSeed = new Uint8Array(this.seed)
+ const thisSeed = new Uint8Array(this.#seed)
for (let i = 0; i < userSeed.byteLength; i++) {
diff |= userSeed[i] ^ thisSeed[i]
}
if (mnemonicPhrase != null) {
let diff = 0
const userMnemonic = utf8.toBytes(mnemonicPhrase)
- const thisMnemonic = new Uint8Array(this.mnemonic ?? [])
+ const thisMnemonic = new Uint8Array(this.#mnemonic ?? [])
for (let i = 0; i < userMnemonic.byteLength; i++) {
diff |= userMnemonic[i] ^ thisMnemonic[i]
}
}
}
- static _createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
+ #createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
if (password == null) {
return Promise.resolve(undefined)
}
})
}
- static _decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
+ #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
const seedLength = type === 'BIP-44' ? 64 : 32
const additionalData = utf8.toBytes(type)
return crypto.subtle
.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
.then(decrypted => {
- this.seed = decrypted.slice(0, seedLength)
- this.mnemonic = decrypted.slice(seedLength)
+ this.#seed = decrypted.slice(0, seedLength)
+ this.#mnemonic = decrypted.slice(seedLength)
new Uint8Array(decrypted).fill(0)
})
}
* @param {number} index - 4-byte index of account to derive
* @returns {ArrayBuffer} Private key for the account
*/
- static _deriveBlake2bPrivateKey (seed: ArrayBuffer, index: number): ArrayBuffer {
+ #deriveBlake2bPrivateKey (seed: ArrayBuffer, index: number): ArrayBuffer {
const b = new ArrayBuffer(4)
new DataView(b).setUint32(0, index, false)
const s = new Uint8Array(seed)
return new Blake2b(32).update(s).update(i).digest().buffer
}
- static _encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
- if (this.type == null) {
+ #encryptWallet (key: CryptoKey): Promise<NamedData<ArrayBuffer>> {
+ if (this.#type == null) {
throw new Error('Invalid wallet type')
}
- if (this.seed == null) {
+ if (this.#seed == null) {
throw new Error('Wallet seed not found')
}
- const seed = new Uint8Array(this.seed)
- const mnemonic = new Uint8Array(this.mnemonic ?? [])
+ const seed = new Uint8Array(this.#seed)
+ const mnemonic = new Uint8Array(this.#mnemonic ?? [])
// restrict iv to 96 bits per GCM best practice
const iv = crypto.getRandomValues(new Uint8Array(12)).buffer
- const additionalData = utf8.toBytes(this.type)
+ const additionalData = utf8.toBytes(this.#type)
const encoded = new Uint8Array([...seed, ...mnemonic])
return crypto.subtle
.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
/**
* Parse inbound message from main thread into typechecked variables.
*/
- static _extractData (message: unknown) {
+ #extractData (message: unknown) {
try {
// Message itself
if (message == null) {
: 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)
+ return this.#createAesKey(action === 'unlock' ? 'decrypt' : 'encrypt', keySalt, password)
.then(key => {
if (password?.detached === false) {
new Uint8Array(password).fill(0)
* Encrypts an existing seed or mnemonic+salt and returns the initialization
* vector, salt, and encrypted data representing the wallet in a locked state.
*/
- static _load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+ #load (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
try {
- if (!this.locked) {
+ if (!this.#locked) {
throw new Error('Wallet is in use')
}
if (key == null || keySalt == null) {
throw new RangeError('Seed for BLAKE2b wallet must be 32 bytes')
}
}
- this.type = type
+ this.#type = type
let seed: Promise<ArrayBuffer>
if (secret instanceof ArrayBuffer) {
if (type === 'BLAKE2b') {
seed = Bip39.fromEntropy(new Uint8Array(secret))
.then(bip39 => {
- this.mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+ this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
return secret
})
} else {
} else {
seed = Bip39.fromPhrase(secret)
.then(bip39 => {
- this.mnemonic = utf8.toBuffer(bip39.phrase ?? '')
+ this.#mnemonic = utf8.toBuffer(bip39.phrase ?? '')
const derive = type === 'BIP-44'
? bip39.toBip39Seed(mnemonicSalt ?? '')
: Promise.resolve(bip39.toBlake2bSeed())
})
}
return seed.then(seed => {
- this.seed = seed
- return this._encryptWallet(key)
+ this.#seed = seed
+ return this.#encryptWallet(key)
.then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted }))
})
} catch (err) {