NODE: process.exit()
}
case 'create': {
- result = await this.create(type, key)
+ result = await this.create(type, key, keySalt, mnemonicSalt)
break
}
case 'derive': {
break
}
case 'import': {
- result = await this.import(type, key, mnemonicPhrase ?? seed, mnemonicSalt)
+ result = await this.import(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
break
}
case 'lock': {
* 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 async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
+ static async create (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
try {
const entropy = crypto.getRandomValues(new Uint8Array(32))
const mnemonicPhrase = (await Bip39Mnemonic.fromEntropy(entropy)).phrase
- return await this.import(type, key, mnemonicPhrase, mnemonicSalt)
+ return await this.import(type, key, keySalt, mnemonicPhrase, mnemonicSalt)
} catch (err) {
throw new Error('Failed to unlock wallet', { cause: err })
}
* wallet seed at a specified index and then returns the public key. The wallet
* must be unlocked prior to derivation.
*/
- static async derive (index?: number): Promise<NamedData<ArrayBuffer>> {
+ static async derive (index?: number): Promise<NamedData<number | ArrayBuffer>> {
try {
if (this.#locked) {
throw new Error('Wallet is locked')
? await Bip44Ckd.nanoCKD(this.#seed, index)
: await Blake2bCkd.ckd(this.#seed, index)
const pub = await NanoNaCl.convert(new Uint8Array(prv))
- return { publicKey: pub.buffer }
+ return { index, publicKey: pub.buffer }
} catch (err) {
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 async import (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, secret?: string | ArrayBuffer, salt?: string): Promise<NamedData<ArrayBuffer>> {
+ static async import (type?: 'BIP-44' | 'BLAKE2b', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<NamedData<ArrayBuffer>> {
try {
if (!this.#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') {
throw new TypeError('Invalid wallet type')
}
- if (key == null) {
- throw new TypeError('Wallet password is required')
- }
if (secret == null) {
throw new TypeError('Seed or mnemonic is required')
}
- if (typeof secret !== 'string' && salt !== undefined) {
+ if (typeof secret !== 'string' && mnemonicSalt !== undefined) {
throw new TypeError('Mnemonic must be a string')
}
if (type === 'BIP-44') {
} else {
this.#mnemonic = await Bip39Mnemonic.fromPhrase(secret)
this.#seed = type === 'BIP-44'
- ? (await this.#mnemonic.toBip39Seed(salt ?? '')).buffer
+ ? (await this.#mnemonic.toBip39Seed(mnemonicSalt ?? '')).buffer
: (await this.#mnemonic.toBlake2bSeed()).buffer
}
- return await this.#encryptWallet(key)
+ const { iv, encrypted } = await this.#encryptWallet(key)
+ return { iv, salt: keySalt, encrypted }
} catch (err) {
this.lock()
throw new Error('Failed to import wallet', { cause: err })
}
}
- static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, salt: ArrayBuffer): Promise<CryptoKey> {
+ static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise<CryptoKey> {
const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
new Uint8Array(password).fill(0).buffer.transfer()
const derivationAlgorithm: Pbkdf2Params = {
name: 'PBKDF2',
hash: 'SHA-512',
iterations: 210000,
- salt
+ salt: keySalt
}
const derivedKeyType: AesKeyGenParams = {
name: 'AES-GCM',
throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
}\r
Bip44Wallet.#isInternal = false\r
- super(id, 'BIP-44', seed, mnemonic)\r
+ super(id.hex, 'BIP-44')\r
}\r
\r
/**\r
Bip44Wallet.#isInternal = true\r
return new this(await Entropy.import(id))\r
}\r
-\r
- /**\r
- * Derives BIP-44 Nano account private keys.\r
- *\r
- * @param {number[]} indexes - Indexes of the accounts\r
- * @returns {Promise<Account>}\r
- */\r
- async ckd (indexes: number[]): Promise<KeyPair[]> {\r
- if (this.isLocked) {\r
- throw new Error('wallet must be unlocked to derive accounts')\r
- }\r
- const results = await SafeWorker.request({\r
- action: 'derive',\r
- type: 'BIP-44',\r
- indexes,\r
- seed: hex.toBuffer(this.seed)\r
- })\r
- const privateKeys: KeyPair[] = []\r
- for (const i of Object.keys(results)) {\r
- if (results[i] == null || !(results[i] instanceof ArrayBuffer)) {\r
- throw new Error('Failed to derive private keys')\r
- }\r
- const privateKey = new Uint8Array(results[i])\r
- privateKeys.push({ index: +i, privateKey })\r
- }\r
- return privateKeys\r
- }\r
}\r
throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
}\r
Blake2bWallet.#isInternal = false\r
- super(id, 'BLAKE2b', seed, mnemonic)\r
+ super(id.hex, 'BLAKE2b')\r
}\r
\r
/**\r
Blake2bWallet.#isInternal = true\r
return new this(await Entropy.import(id))\r
}\r
-\r
- /**\r
- * Derives BLAKE2b account private keys.\r
- *\r
- * @param {number[]} indexes - Indexes of the accounts\r
- * @returns {Promise<Account>}\r
- */\r
- async ckd (indexes: number[]): Promise<KeyPair[]> {\r
- if (this.isLocked) {\r
- throw new Error('wallet must be unlocked to derive accounts')\r
- }\r
- const results = []\r
- for (const index of indexes) {\r
- const indexHex = index.toString(16).padStart(8, '0').toUpperCase()\r
- const inputHex = `${this.seed}${indexHex}`.padStart(72, '0')\r
- const inputBytes = hex.toBytes(inputHex)\r
- const privateKey = new Blake2b(32).update(inputBytes).digest()\r
- results.push({ index, privateKey })\r
- }\r
- return results\r
- }\r
}\r
throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`)\r
}\r
LedgerWallet.#isInternal = false\r
- super(id, 'Ledger')\r
+ super(id.hex, 'Ledger')\r
}\r
\r
/**\r
* types of wallets are supported, each as a derived class: Bip44Wallet,\r
* Blake2bWallet, LedgerWallet.\r
*/\r
-export abstract class Wallet {\r
- abstract ckd (index: number[]): Promise<KeyPair[]>\r
-\r
+export class Wallet {\r
static #DB_NAME = 'Wallet'\r
\r
+ /**\r
+ * Creates a new HD wallet by using an entropy value generated using a\r
+ * cryptographically strong pseudorandom number generator.\r
+ *\r
+ * @param {string} password - Encrypts the wallet to lock and unlock it\r
+ * @param {string} [salt=''] - Used when generating the final seed\r
+ * @returns {Bip44Wallet} A newly instantiated Wallet\r
+ */\r
+ static async create (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise<Wallet> {\r
+ try {\r
+ const { iv, salt, encrypted } = await SafeWorker.request<ArrayBuffer>({\r
+ action: 'create',\r
+ type,\r
+ password: utf8.toBuffer(password),\r
+ mnemonicSalt: mnemonicSalt ?? ''\r
+ })\r
+ const encoded = JSON.stringify({\r
+ type,\r
+ iv: bytes.toHex(new Uint8Array(iv)),\r
+ salt: bytes.toHex(new Uint8Array(salt)),\r
+ encrypted: bytes.toHex(new Uint8Array(encrypted))\r
+ })\r
+ await Database.put({ [name]: encoded }, this.#DB_NAME)\r
+ return new this(name, type)\r
+ } catch (err) {\r
+ throw new Error('Error creating new Bip44Wallet', { cause: err })\r
+ }\r
+ }\r
+\r
#accounts: AccountList\r
- #id: Entropy\r
- #locked: boolean\r
#lockTimer?: any\r
- #m?: Bip39Mnemonic\r
- #s?: Uint8Array<ArrayBuffer>\r
+ #name: string\r
#type: WalletType\r
\r
- get id () { return `${this.type}_${this.#id.hex}` }\r
- get isLocked () { return this.#locked }\r
- get isUnlocked () { return !this.#locked }\r
- get mnemonic () {\r
- if (this.#locked || this.#m == null) throw new Error('failed to get mnemonic', { cause: 'wallet locked' })\r
- return this.#m.phrase\r
- }\r
- get seed () {\r
- if (this.#locked || this.#s == null) throw new Error('failed to get seed', { cause: 'wallet locked' })\r
- return bytes.toHex(this.#s)\r
- }\r
+ get name () { return `${this.type}_${this.#name}` }\r
get type () { return this.#type }\r
\r
- constructor (id: Entropy, type: WalletType, seed?: Uint8Array<ArrayBuffer>, mnemonic?: Bip39Mnemonic) {\r
+ constructor (name: string, type: WalletType) {\r
if (this.constructor === Wallet) {\r
throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
}\r
this.#accounts = new AccountList()\r
- this.#id = id\r
- this.#locked = false\r
- this.#m = mnemonic\r
- this.#s = seed\r
+ this.#name = name\r
this.#type = type\r
}\r
\r
}\r
}\r
if (indexes.length > 0) {\r
- const keypairs = await this.ckd(indexes)\r
- const privateKeys: KeyPair[] = []\r
- const publicKeys: KeyPair[] = []\r
- for (const keypair of keypairs) {\r
- const { index, privateKey, publicKey } = keypair\r
- if (index == null) {\r
- throw new RangeError('Account keys derived but index missing')\r
- }\r
- if (privateKey != null) {\r
- privateKeys.push(keypair)\r
- } else if (publicKey != null) {\r
- publicKeys.push(keypair)\r
- }\r
+ const promises = []\r
+ for (const index of indexes) {\r
+ promises.push(SafeWorker.request<ArrayBuffer>({\r
+ action: 'derive',\r
+ index\r
+ }))\r
}\r
- const privateAccounts = privateKeys.length > 0\r
- ? await Account.import(privateKeys, this.seed)\r
- : []\r
+ const publicKeys = await Promise.all(promises)\r
const publicAccounts = publicKeys.length > 0\r
? Account.import(publicKeys)\r
: []\r
- const accounts = [...privateAccounts, ...publicAccounts]\r
+ const accounts = [...publicAccounts]\r
for (const a of accounts) {\r
if (a.index == null) {\r
throw new RangeError('Index missing for Account')\r
*/\r
async destroy (): Promise<void> {\r
try {\r
- this.#m?.destroy()\r
- bytes.erase(this.#s)\r
- this.#m = undefined\r
- this.#s = undefined\r
- for (const a in this.#accounts) {\r
- this.#accounts[a].destroy()\r
- delete this.#accounts[a]\r
- }\r
- await Database.delete(this.id, Wallet.#DB_NAME)\r
+ await Database.delete(this.name, Wallet.#DB_NAME)\r
} catch (err) {\r
console.error(err)\r
throw new Error('failed to destroy wallet', { cause: err })\r
const { isLocked } = await SafeWorker.request<boolean>({\r
action: 'lock'\r
})\r
- this.#locked = isLocked\r
- return this.#locked\r
+ if (!isLocked) {\r
+ throw new Error('Lock request to Safe failed')\r
+ }\r
+ clearTimeout(this.#lockTimer)\r
+ return isLocked\r
} catch (err) {\r
- throw new Error('failed to lock wallet', { cause: err })\r
+ throw new Error('Failed to lock wallet', { cause: err })\r
}\r
}\r
\r
* @returns {Promise<string>} Hexadecimal-formatted 64-byte signature\r
*/\r
async sign (index: number, block: ChangeBlock | ReceiveBlock | SendBlock): Promise<string> {\r
- if (this.#locked) throw new Error('Wallet must be unlocked to sign')\r
- if (this.#s == null) throw new Error('Wallet seed not found')\r
try {\r
const { signature } = await SafeWorker.request<ArrayBuffer>({\r
action: 'sign',\r
})\r
const sig = bytes.toHex(new Uint8Array(signature))\r
block.signature = sig\r
+ clearTimeout(this.#lockTimer)\r
+ this.#lockTimer = setTimeout(() => this.lock(), 120)\r
return sig\r
} catch (err) {\r
throw new Error(`Failed to sign block`, { cause: err })\r
* @param {string} password Used previously to lock the wallet\r
* @returns True if successfully unlocked\r
*/\r
- async unlock (password: string, iv: ArrayBuffer, salt: ArrayBuffer): Promise<boolean> {\r
+ async unlock (password: string): Promise<boolean> {\r
try {\r
- if (typeof password !== 'string') {\r
- throw new TypeError('Invalid password')\r
- }\r
- const unlockRequest = SafeWorker.request<boolean>({\r
+ const record = await Database.get<string>(this.#name, Wallet.#DB_NAME)\r
+ const decoded = JSON.parse(record[this.#name])\r
+ const iv: ArrayBuffer = hex.toBuffer(decoded.iv)\r
+ const salt: ArrayBuffer = hex.toBuffer(decoded.salt)\r
+ const encrypted: ArrayBuffer = hex.toBuffer(decoded.encrypted)\r
+ const { isUnlocked } = await SafeWorker.request<boolean>({\r
action: 'unlock',\r
password: utf8.toBuffer(password),\r
iv,\r
- salt\r
+ salt,\r
+ encrypted\r
})\r
- password = ''\r
- const { isUnlocked } = await unlockRequest\r
- if (isUnlocked) {\r
- this.#lockTimer = setTimeout(this.lock, 120)\r
- } else {\r
- throw new Error('Request to wallet worker failed')\r
+ if (!isUnlocked) {\r
+ throw new Error('Unlock request to Safe failed')\r
}\r
- this.#locked = isUnlocked\r
- return true\r
+ clearTimeout(this.#lockTimer)\r
+ this.#lockTimer = setTimeout(() => this.lock(), 120)\r
+ return isUnlocked\r
} catch (err) {\r
throw new Error('Failed to unlock wallet', { cause: err })\r
}\r
import { Rolodex } from './lib/rolodex'
import { Rpc } from './lib/rpc'
import { Tools } from './lib/tools'
-import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './lib/wallets'
+import { Wallet, Bip44Wallet, Blake2bWallet, LedgerWallet } from './lib/wallets'
export {
Account,
Rolodex,
Rpc,
Tools,
- Bip44Wallet, Blake2bWallet, LedgerWallet
+ Wallet, Bip44Wallet, Blake2bWallet, LedgerWallet
}
* @type {typeof import('../dist/types.d.ts').Blake2bWallet}\r
*/\r
let Blake2bWallet\r
+/**\r
+* @type {typeof import('../dist/types.d.ts').Wallet}\r
+*/\r
+let Wallet\r
if (isNode) {\r
- ({ Bip44Wallet, Blake2bWallet } = await import('../dist/nodejs.min.js'))\r
+ ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/nodejs.min.js'))\r
} else {\r
- ({ Bip44Wallet, Blake2bWallet } = await import('../dist/browser.min.js'))\r
+ ({ Bip44Wallet, Blake2bWallet, Wallet } = await import('../dist/browser.min.js'))\r
}\r
\r
await Promise.all([\r
suite('Create wallets', async () => {\r
\r
await test('destroy BIP-44 wallet before unlocking', async () => {\r
- const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Wallet.create('BIP-44', NANO_TEST_VECTORS.PASSWORD)\r
await assert.resolves(wallet.destroy())\r
\r
assert.ok('mnemonic' in wallet)\r