* Cross-platform worker for managing wallet secrets.
*/
export class VaultWorker {
- static #locked: boolean = true
- static #timeout: VaultTimer
- static #type?: 'BIP-44' | 'BLAKE2b'
- static #seed?: ArrayBuffer
- static #mnemonic?: ArrayBuffer
- static #parentPort?: any
+ static locked: boolean = true
+ static timeout: VaultTimer
+ static type?: 'BIP-44' | 'BLAKE2b'
+ static seed?: ArrayBuffer
+ static mnemonic?: ArrayBuffer
+ static parentPort?: any
static {
- NODE: this.#parentPort = parentPort
+ NODE: this.parentPort = parentPort
}
static {
- NODE: this.#parentPort = parentPort
+ NODE: this.parentPort = parentPort
const listener = (message: MessageEvent<any>): Promise<void> => {
- return this.#extractData(message.data).then(extracted => {
+ return this._extractData(message.data).then(extracted => {
const {
action,
type,
})
}
BROWSER: addEventListener('message', listener)
- NODE: this.#parentPort?.on('message', listener)
+ NODE: this.parentPort?.on('message', listener)
}
/**
const entropy = crypto.getRandomValues(new Uint8Array(32))
return Bip39.fromEntropy(entropy)
.then(bip39 => {
- return this.#load(type, key, keySalt, bip39.phrase, mnemonicSalt)
+ return this._load(type, key, keySalt, bip39.phrase, mnemonicSalt)
.then(record => {
- if (this.#seed == null || this.#mnemonic == null) {
+ if (this.seed == null || this.mnemonic == null) {
throw new Error('Failed to generate seed and mnemonic')
}
- return { ...record, seed: this.#seed.slice(), mnemonic: this.#mnemonic.slice() }
+ return { ...record, seed: this.seed.slice(), mnemonic: this.mnemonic.slice() }
})
})
} catch (err) {
*/
static 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 })
}
}
* 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)
+ 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
}
static lock (): NamedData<boolean> {
- this.#mnemonic = undefined
- this.#seed = undefined
- this.#locked = true
- this.#timeout?.pause()
- return { isLocked: this.#locked }
+ this.mnemonic = undefined
+ this.seed = undefined
+ this.locked = true
+ this.timeout?.pause()
+ return { isLocked: this.locked }
}
/**
*/
static 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 })
}
}
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 })
})
}
*/
static 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 })
}
}
*/
static 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> {
+ static _createAesKey (purpose: 'encrypt' | 'decrypt', keySalt: ArrayBuffer, password?: ArrayBuffer): Promise<CryptoKey | undefined> {
return new Promise((resolve, reject): void => {
if (password == null) {
resolve(undefined)
})
}
- static #decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
+ static _decryptWallet (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<void> {
const seedLength = type === 'BIP-44' ? 64 : 32
const additionalData = utf8.toBytes(type)
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 {
+ static _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) {
+ static _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)
.then(encrypted => {
/**
* Parse inbound message from main thread into typechecked variables.
*/
- static #extractData (message: unknown) {
+ static _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>> {
+ static _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 }) => {
return { iv, salt: keySalt, encrypted }
})