From: Chris Duncan Date: Wed, 16 Jul 2025 21:58:18 +0000 (-0700) Subject: Refactor to support multiple stores at once. X-Git-Tag: v0.10.5~56^2~26 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=0345e1fb776f55d3fb1d9eb79f0660039307f8a4;p=libnemo.git Refactor to support multiple stores at once. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index b892016..dfa63ba 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later import { Blake2b } from './blake2b' +import { ChangeBlock, ReceiveBlock, SendBlock } from './block' import { ACCOUNT_KEY_BYTE_LENGTH, ACCOUNT_KEY_HEX_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants' import { base32, bytes, hex, utf8 } from './convert' import { Rpc } from './rpc' +import { Data } from '#types' import { NanoNaClWorker, SafeWorker } from '#workers' -import { ChangeBlock, ReceiveBlock, SendBlock } from './block' /** * Represents a single Nano address and the associated public key. To include the @@ -19,24 +20,18 @@ export class Account { #address: string #publicKey: Uint8Array - #privateKey: Uint8Array #balance?: bigint #frontier?: string - #index?: number #receivable?: bigint #representative?: Account #weight?: bigint get address () { return `${PREFIX}${this.#address}` } - get isLocked () { return this.#privateKey.buffer.detached } - get isUnlocked () { return !this.isLocked } get publicKey () { return bytes.toHex(this.#publicKey) } - get privateKey () { return bytes.toHex(this.#privateKey) } get balance () { return this.#balance } get frontier () { return this.#frontier } - get index () { return this.#index } get receivable () { return this.#receivable } get representative () { return this.#representative } get weight () { return this.#weight } @@ -48,27 +43,21 @@ export class Account { if (v instanceof Account) { this.#representative = v } else if (typeof v === 'string') { - this.#representative = Account.fromAddress(v) + this.#representative = Account.import(v) } else { throw new TypeError(`Invalid argument for account representative: ${v}`) } } set weight (v) { this.#weight = v ? BigInt(v) : undefined } - constructor (address: string, publicKey: Uint8Array, index?: number) { + constructor (address: string, publicKey: Uint8Array) { if (!Account.#isInternal) { throw new Error(`Account cannot be instantiated directly. Use factory methods instead.`) } - if (index !== undefined && typeof index !== 'number') { - throw new TypeError(`Invalid index ${index} when creating Account ${address}`) - } this.#address = address .replace(PREFIX, '') .replace(PREFIX_LEGACY, '') - this.#index = index this.#publicKey = publicKey - this.#privateKey = new Uint8Array(0) - bytes.erase(this.#privateKey) } /** @@ -76,12 +65,11 @@ export class Account { * allow garbage collection. */ async destroy (): Promise { - bytes.erase(this.#privateKey) await SafeWorker.add({ + store: 'Account', method: 'destroy', name: this.#publicKey }) - this.#index = undefined this.#frontier = undefined this.#balance = undefined this.#receivable = undefined @@ -89,18 +77,136 @@ export class Account { this.#weight = undefined } + + /** + * Instantiates an Account object from its Nano address. + * + * @param {string} addresses - Address of the account + * @returns {Account} The instantiated Account object + */ + static import (address: string): Account + /** + * Instantiates Account objects from their Nano addresses. + * + * @param {string[]} addresses - Addresses of the accounts + * @returns {Account[]} The instantiated Account objects + */ + static import (addresses: string[]): Account[] + /** + * Instantiates an Account object from its public key. It is unable to sign + * blocks or messages since it has no private key. + * + * @param {string} publicKey - Public key of the account + * @returns {Account} The instantiated Account object + */ + static import (publicKey: string): Account + /** + * Instantiates an Account object from its public key. It is unable to sign + * blocks or messages since it has no private key. + * + * @param {Uint8Array} publicKey - Public key of the account + * @returns {Account} The instantiated Account object + */ + static import (publicKey: Uint8Array): Account + /** + * Instantiates Account objects from their public keys. They are unable to sign + * blocks or messages since they have no private key. + * + * @param {string[]} publicKeys - Public keys of the accounts + * @returns {Account[]} The instantiated Account objects + */ + static import (publicKeys: string[]): Account[] + /** + * Instantiates Account objects from their public keys. They are unable to sign + * blocks or messages since they have no private key. + * + * @param {Uint8Array[]} publicKeys - Public keys of the accounts + * @returns {Account[]} The instantiated Account objects + */ + static import (publicKeys: Uint8Array[]): Account[] + /** + * Instantiates an Account object from its private key which is then encrypted + * and stored in IndexedDB. The corresponding public key will automatically be + * derived and saved. + * + * @param {string} privateKey - Private key of the account + * @param {(string|Uint8Array)} password - Used to encrypt the private key + * @returns {Account} A new Account object + */ + static async import (privateKey: string, password: string | Uint8Array): Promise + /** + * Instantiates an Account object from its private key which is then encrypted + * and stored in IndexedDB. The corresponding public key will automatically be + * derived and saved. + * + * @param {Uint8Array} privateKey - Private key of the account + * @param {(string|Uint8Array)} password - Used to encrypt the private key + * @returns {Account} A new Account object + */ + static async import (privateKey: Uint8Array, password: string | Uint8Array): Promise + /** + * Instantiates Account objects from their private keys which are then + * encrypted and stored in IndexedDB. The corresponding public keys will + * automatically be derived and saved. + * + * @param {string[]} privateKeys - Private keys of the account + * @param {(string|Uint8Array)} password - Used to encrypt the private keys + * @returns {Account[]} The instantiated Account objects + */ + static async import (privateKeys: string[], password: string | Uint8Array): Promise + /** + * Instantiates Account objects from their private keys which are then + * encrypted and stored in IndexedDB. The corresponding public keys will + * automatically be derived and saved. + * + * @param {Uint8Array[]} privateKeys - Private keys of the account + * @param {(string|Uint8Array)} password - Used to encrypt the private keys + * @returns {Account[]} The instantiated Account objects + */ + static async import (privateKeys: Uint8Array[], password: string | Uint8Array): Promise + static import (input: string | string[] | Uint8Array | Uint8Array[], password?: string | Uint8Array): Account | Account[] | Promise { + if (Array.isArray(input)) { + if (password != null) { + return new Promise((resolve, reject): void => { + this.#fromPrivateKeys(input, password) + .then(r => resolve(r)) + .catch(e => reject(e)) + }) + } + if (input[0] instanceof Uint8Array || /^[A-Fa-f0-9]{64}$/.test(input[0])) { + return this.#fromPublicKeys(input) + } + return this.#fromAddresses(input as string[]) + } else { + if (password != null) { + return new Promise((resolve, reject): void => { + this.#fromPrivateKeys([input] as string[], password) + .then(r => resolve(r[0])) + .catch(e => reject(e)) + }) + } + if (input instanceof Uint8Array || /^[A-Fa-f0-9]{64}$/.test(input)) { + return this.#fromPublicKeys([input] as string[])[0] + } + return this.#fromAddresses([input] as string[])[0] + } + } + /** * Instantiates an Account object from its Nano address. * * @param {string} address - Address of the account - * @param {number} [index] - Account number used when deriving the address * @returns {Account} The instantiated Account object */ - static fromAddress (address: string, index?: number): Account { - this.#isInternal = true - this.validate(address) - const publicKey = this.#addressToKey(address) - return new this(address, publicKey, index) + static #fromAddresses (addresses: string[]): Account[] { + const accounts: Account[] = [] + for (let address of addresses) { + this.#isInternal = true + this.validate(address) + const publicKey = this.#addressToKey(address) + accounts.push(new this(address, publicKey)) + } + return accounts } /** @@ -108,15 +214,18 @@ export class Account { * blocks or messages since it has no private key. * * @param {(string|Uint8Array)} publicKey - Public key of the account - * @param {number} [index] - Account number used when deriving the key * @returns {Account} The instantiated Account object */ - static fromPublicKey (publicKey: string | Uint8Array, index?: number): Account { - this.#validateKey(publicKey) - if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey) - const address = this.#keyToAddress(publicKey) - this.#isInternal = true - return new this(address, publicKey, index) + static #fromPublicKeys (publicKeys: string[] | Uint8Array[]): Account[] { + const accounts: Account[] = [] + for (let publicKey of publicKeys) { + this.#validateKey(publicKey) + if (typeof publicKey === 'string') publicKey = hex.toBytes(publicKey) + const address = this.#keyToAddress(publicKey) + this.#isInternal = true + accounts.push(new this(address, publicKey)) + } + return accounts } /** @@ -124,48 +233,49 @@ export class Account { * and stored in IndexedDB. The corresponding public key will automatically be * derived and saved. * - * @param {(string|Uint8Array)} privateKey - Private key of the account + * @param {(string|Uint8Array)} privateKeys - Private key of the account * @param {number} [index] - Account number used when deriving the key * @returns {Account} A new Account object */ - static async fromPrivateKey (password: string | Uint8Array, privateKey: string | Uint8Array, index?: number): Promise { + static async #fromPrivateKeys (privateKeys: string[] | Uint8Array[], password: string | Uint8Array): Promise { if (typeof password === 'string') password = utf8.toBytes(password) if (password == null || !(password instanceof Uint8Array)) { throw new Error('Invalid password when importing Account') } - this.#validateKey(privateKey) - if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey) - let publicKey: string - try { - const headers = { - method: 'convert' - } - const data = { - privateKey: new Uint8Array(privateKey).buffer + + const keypairs: Data = {} + for (let privateKey of privateKeys) { + this.#validateKey(privateKey) + if (typeof privateKey === 'string') privateKey = hex.toBytes(privateKey) + let publicKey: string + try { + const headers = { + method: 'convert' + } + const data = { + privateKey: new Uint8Array(privateKey).buffer + } + publicKey = await NanoNaClWorker.add(headers, data) + keypairs[publicKey] = privateKey.buffer + } catch (err) { + throw new Error(`Failed to derive public key from private key`, { cause: err }) } - publicKey = await NanoNaClWorker.add(headers, data) - } catch (err) { - throw new Error(`Failed to derive public key from private key`, { cause: err }) } - const self = await this.fromPublicKey(publicKey, index) + const accounts = await this.#fromPublicKeys(Object.keys(keypairs)) try { const headers = { + store: 'Account', method: 'set', - name: publicKey - } - const data = { - password: password.buffer, - id: hex.toBytes(publicKey).buffer, - privateKey: privateKey.buffer + password: password.buffer } - const isLocked = await SafeWorker.add(headers, data) + const isLocked = await SafeWorker.add(headers, keypairs) if (!isLocked) { throw null } - return self + return accounts } catch (err) { - throw new Error(`Failed to lock Account ${self.address}`, { cause: err }) + throw new Error(`Failed to lock Accounts`, { cause: err }) } finally { bytes.erase(password) } @@ -199,7 +309,7 @@ export class Account { this.#balance = BigInt(balance) this.#frontier = frontier this.#receivable = BigInt(receivable) - this.#representative = Account.fromAddress(representative) + this.#representative = await Account.import(representative) this.#weight = BigInt(weight) } @@ -211,7 +321,7 @@ export class Account { * @param {(ChangeBlock|ReceiveBlock|SendBlock)} block - The block data to be hashed and signed * @returns {Promise} Hexadecimal-formatted 64-byte signature */ - async sign (password: string | Uint8Array, block: ChangeBlock | ReceiveBlock | SendBlock): Promise { + async sign (block: ChangeBlock | ReceiveBlock | SendBlock, password: string | Uint8Array): Promise { const privateKey = await this.exportPrivateKey(password) try { const headers = { @@ -246,25 +356,16 @@ export class Account { } try { const headers = { + store: 'Account', method: 'get', - name: this.publicKey - } - const data = { + name: this.publicKey, password: password.buffer } - const response = await SafeWorker.add(headers, data) - let { id, privateKey } = response - if (id == null) { - throw null - } - id = bytes.toHex(new Uint8Array(id)) - if (id !== this.publicKey) { - throw null - } - const sk = new Uint8Array(privateKey as ArrayBuffer) + const response = await SafeWorker.add(headers) + const privateKey = new Uint8Array(response[this.publicKey] as ArrayBuffer) return format === 'hex' - ? bytes.toHex(sk) - : sk + ? bytes.toHex(privateKey) + : privateKey } catch (err) { throw new Error(`Failed to export private key for Account ${this.address}`, { cause: err }) } finally { diff --git a/src/lib/block.ts b/src/lib/block.ts index 5e35952..55aee7e 100644 --- a/src/lib/block.ts +++ b/src/lib/block.ts @@ -32,7 +32,7 @@ abstract class Block { if (account instanceof Account) { this.account = account } else if (typeof account === 'string') { - this.account = Account.fromAddress(account) + this.account = Account.import(account) } else { throw new TypeError('Invalid account') } @@ -141,9 +141,9 @@ abstract class Block { } else { try { const account = (typeof input === 'string') - ? await Account.fromPrivateKey('', input) + ? await Account.import(input, '') : this.account - this.signature = await account.sign('', this) + this.signature = await account.sign(this, '') } catch (err) { throw new Error(`Failed to sign block`, { cause: err }) } @@ -186,6 +186,7 @@ abstract class Block { * @returns {boolean} True if block was signed by the matching private key */ async verify (key?: string): Promise { + debugger key ??= this.account.publicKey if (!key) { throw new Error('Provide a key for block signature verification.') @@ -229,12 +230,12 @@ export class SendBlock extends Block { frontier: string, work?: string ) { - if (typeof sender === 'string') sender = Account.fromAddress(sender) - if (typeof representative === 'string') representative = Account.fromAddress(representative) + if (typeof sender === 'string') sender = Account.import(sender) + if (typeof representative === 'string') representative = Account.import(representative) super(sender) this.previous = frontier this.representative = representative - this.link = Account.fromAddress(recipient).publicKey + this.link = Account.import(recipient).publicKey this.work = work ?? '' const bigBalance = BigInt(balance) @@ -268,8 +269,8 @@ export class ReceiveBlock extends Block { frontier?: string, work?: string ) { - if (typeof recipient === 'string') recipient = Account.fromAddress(recipient) - if (typeof representative === 'string') representative = Account.fromAddress(representative) + if (typeof recipient === 'string') recipient = Account.import(recipient) + if (typeof representative === 'string') representative = Account.import(representative) super(recipient) this.previous = frontier ?? recipient.publicKey this.representative = representative @@ -295,7 +296,7 @@ export class ChangeBlock extends Block { previous: string representative: Account balance: bigint - link: string = Account.fromAddress(BURN_ADDRESS).publicKey + link: string = Account.import(BURN_ADDRESS).publicKey signature?: string work?: string @@ -306,8 +307,8 @@ export class ChangeBlock extends Block { frontier: string, work?: string ) { - if (typeof account === 'string') account = Account.fromAddress(account) - if (typeof representative === 'string') representative = Account.fromAddress(representative) + if (typeof account === 'string') account = Account.import(account) + if (typeof representative === 'string') representative = Account.import(representative) super(account) this.previous = frontier this.representative = representative diff --git a/src/lib/rolodex.ts b/src/lib/rolodex.ts index aa72f70..28a800a 100644 --- a/src/lib/rolodex.ts +++ b/src/lib/rolodex.ts @@ -44,7 +44,7 @@ export class Rolodex { .replaceAll('<', '\\u003c') .replaceAll('>', '\\u003d') .replaceAll('\\', '\\u005c') - const account = Account.fromAddress(address) + const account = Account.import(address) const nameResult = this.#entries.find(e => e.name === name) const accountResult = this.#entries.find(e => e.account.address === address) if (!accountResult) { diff --git a/src/lib/tools.ts b/src/lib/tools.ts index 41caeb1..72ab3b2 100644 --- a/src/lib/tools.ts +++ b/src/lib/tools.ts @@ -136,7 +136,7 @@ export async function sweep ( const blockQueue: Promise[] = [] const results: { status: 'success' | 'error', address: string, message: string }[] = [] - const recipientAccount = Account.fromAddress(recipient) + const recipientAccount = Account.import(recipient) const accounts = await wallet.refresh(rpc, from, to) for (const account of accounts) { if (account.representative?.address && account.frontier) { diff --git a/src/lib/wallets/wallet.ts b/src/lib/wallets/wallet.ts index 12eedf6..46842cb 100644 --- a/src/lib/wallets/wallet.ts +++ b/src/lib/wallets/wallet.ts @@ -4,7 +4,7 @@ import { Account, AccountList } from '#src/lib/account.js' import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js' import { ADDRESS_GAP } from '#src/lib/constants.js' -import { bytes, utf8 } from '#src/lib/convert.js' +import { bytes, hex, utf8 } from '#src/lib/convert.js' import { Entropy } from '#src/lib/entropy.js' import { Rpc } from '#src/lib/rpc.js' import { Data, KeyPair } from '#types' @@ -29,7 +29,7 @@ export abstract class Wallet { get id () { return `libnemo_${this.#id.hex}` } get isLocked () { return this.#locked } get isUnlocked () { return !this.#locked } - get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : '' } + get mnemonic () { return this.#m instanceof Bip39Mnemonic ? this.#m.phrase : null } get seed () { return bytes.toHex(this.#s) } constructor (id: Entropy, seed?: Uint8Array, mnemonic?: Bip39Mnemonic) { @@ -106,9 +106,9 @@ export abstract class Wallet { const { privateKey, publicKey, index } = keypair if (index == null) throw new RangeError('Account keys derived but index missing') if (privateKey != null) { - output[index] = await Account.fromPrivateKey(this.seed, privateKey, index) + output[index] = await Account.import(privateKey, this.seed) } else if (publicKey != null) { - output[index] = await Account.fromPublicKey(publicKey, index) + output[index] = await Account.import(publicKey) } else { throw new RangeError('Account keys missing') } @@ -132,6 +132,7 @@ export abstract class Wallet { this.#m = null bytes.erase(this.#s) await SafeWorker.add({ + store: 'Wallet', method: 'destroy', name: this.id }) @@ -152,22 +153,19 @@ export abstract class Wallet { throw new Error('Failed to lock wallet') } try { - // const promises = [] - // for (const account of this.#accounts) { - // promises.push(account.lock(this.seed)) - // } - // await Promise.all(promises) + const serialized = JSON.stringify({ + id: this.id, + mnemonic: this.mnemonic, + seed: this.seed + }) + const encoded = utf8.toBytes(serialized) const headers = { + store: 'Wallet', method: 'set', - name: this.id + password: new Uint8Array(password).buffer } const data: Data = { - password: new Uint8Array(password).buffer, - id: new Uint8Array(this.#id.bytes).buffer, - seed: this.#s.buffer - } - if (this.#m != null) { - data.mnemonic = utf8.toBytes(this.#m?.phrase ?? '').buffer + [this.id]: encoded.buffer } const success = await SafeWorker.add(headers, data) if (!success) { @@ -178,6 +176,7 @@ export abstract class Wallet { } finally { bytes.erase(password) } + bytes.erase(this.#s) this.#m = null this.#locked = true return true @@ -225,34 +224,30 @@ export abstract class Wallet { } try { const headers = { + store: 'Wallet', method: 'get', - name: this.id - } - const data = { + name: this.id, password: new Uint8Array(password).buffer } - const response = await SafeWorker.add(headers, data) - let { id, mnemonic, seed } = response + const response = await SafeWorker.add(headers) + const decoded = bytes.toUtf8(response[this.id]) + const deserialized = JSON.parse(decoded) + let { id, mnemonic, seed } = deserialized if (id == null) { - throw null + throw new Error('ID is null') } - id = await Entropy.import(id as ArrayBuffer) + id = await Entropy.import(id.replace('libnemo_', '')) if (id.hex !== this.#id.hex) { - throw null + throw new Error('ID does not match') } if (mnemonic != null) { - this.#m = await Bip39Mnemonic.fromPhrase(bytes.toUtf8(mnemonic)) + this.#m = await Bip39Mnemonic.fromPhrase(mnemonic) mnemonic = null } if (seed != null) { - this.#s = new Uint8Array(seed as ArrayBuffer) + this.#s = hex.toBytes(seed) seed = null } - // const promises = [] - // for (const account of this.#accounts) { - // promises.push(account.exportPrivateKey(this.seed)) - // } - // await Promise.all(promises) } catch (err) { throw new Error('Failed to unlock wallet') } finally { @@ -287,7 +282,7 @@ export abstract class Wallet { for (const key of Object.keys(errors ?? {})) { const value = errors[key] if (value === 'Account not found') { - return Account.fromAddress(key) + return Account.import(key) } } return await this.unopened(rpc, batchSize, from + batchSize) diff --git a/src/lib/workers/bip44-ckd.ts b/src/lib/workers/bip44-ckd.ts index d5dd65c..6f73579 100644 --- a/src/lib/workers/bip44-ckd.ts +++ b/src/lib/workers/bip44-ckd.ts @@ -35,7 +35,7 @@ export class Bip44Ckd extends WorkerInterface { const pk = await this.ckd(seed, coin, i) privateKeys[i] = pk } catch (err) { - console.log(err) + console.log('BIP-44 error') } } return privateKeys diff --git a/src/lib/workers/queue.ts b/src/lib/workers/queue.ts index 2606f45..608eef9 100644 --- a/src/lib/workers/queue.ts +++ b/src/lib/workers/queue.ts @@ -39,8 +39,7 @@ export class Queue { this.#url = URL.createObjectURL(new Blob([worker], { type: 'text/javascript' })) this.#worker = new Worker(this.#url, { type: 'module' }) this.#worker.addEventListener('message', message => { - let result = message.data - this.#report(result) + this.#report(message.data) }) Queue.#instances.push(this) } @@ -79,6 +78,13 @@ export class Queue { const { id, headers, data, reject } = this.#job try { const buffers: ArrayBuffer[] = [] + if (headers != null) { + for (let h of Object.keys(headers)) { + if (headers[h] instanceof ArrayBuffer) { + buffers.push(headers[h]) + } + } + } if (data != null) { for (let d of Object.keys(data)) { buffers.push(data[d]) diff --git a/src/lib/workers/safe.ts b/src/lib/workers/safe.ts index f67733b..970b0df 100644 --- a/src/lib/workers/safe.ts +++ b/src/lib/workers/safe.ts @@ -13,10 +13,8 @@ import { Data, Headers, SafeRecord } from '#types' */ export class Safe extends WorkerInterface { static DB_NAME = 'libnemo' - static STORE_NAME = 'Safe' + static DB_STORES = ['Wallet', 'Account'] static ERR_MSG = 'Failed to store item in Safe' - static #decoder: TextDecoder = new TextDecoder() - static #encoder: TextEncoder = new TextEncoder() static #storage: IDBDatabase static { @@ -24,28 +22,28 @@ export class Safe extends WorkerInterface { } static async work (headers: Headers, data: Data): Promise { + const { method, name, password, store } = headers this.#storage = await this.#open(this.DB_NAME) - const { method, name } = headers let result try { switch (method) { case 'set': { - result = await this.set(name, data) + result = await this.set(store, password, data) break } case 'get': { - result = await this.get(name, data) + result = await this.get(store, password, name) break } case 'destroy': { - result = await this.destroy(name) + result = await this.destroy(store, name) break } default: { result = `unknown Safe method ${method}` } } - } catch { + } catch (err) { result = false } return result @@ -54,9 +52,9 @@ export class Safe extends WorkerInterface { /** * Removes data from the Safe without decrypting. */ - static async destroy (name: string): Promise { + static async destroy (store: string, name: string): Promise { try { - return await this.#delete(name) + return await this.#delete(store, name) } catch { throw new Error(this.ERR_MSG) } @@ -65,44 +63,40 @@ export class Safe extends WorkerInterface { /** * Encrypts data with a password byte array and stores it in the Safe. */ - static async set (name: string, data: Data): Promise { - const { password } = data - delete data.password - + static async set (store: string, password: ArrayBuffer, data: Data): Promise { try { const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - if (this.#isInvalid(name, derivationKey, data)) { + if (this.#isInvalid(store, derivationKey, data)) { throw new Error('Failed to import key') } - const base32: { [key: string]: string } = {} - for (const d of Object.keys(data)) { - base32[d] = bytes.toBase32(new Uint8Array(data[d])) - } - const serialized = JSON.stringify(base32) - const encoded = this.#encoder.encode(serialized) - - const salt = await Entropy.create() - const derivationAlgorithm: Pbkdf2Params = { - name: 'PBKDF2', - hash: 'SHA-512', - salt: salt.bytes, - iterations: 210000 - } - const derivedKeyType: AesKeyGenParams = { - name: 'AES-GCM', - length: 256 + const records: SafeRecord[] = [] + for (const label of Object.keys(data)) { + const salt = await Entropy.create() + const derivationAlgorithm: Pbkdf2Params = { + name: 'PBKDF2', + hash: 'SHA-512', + salt: salt.bytes, + iterations: 210000 + } + const derivedKeyType: AesKeyGenParams = { + name: 'AES-GCM', + length: 256 + } + const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt']) + + const iv = await Entropy.create() + const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, data[label]) + const record: SafeRecord = { + iv: iv.hex, + salt: salt.hex, + label, + encrypted + } + records.push(record) } - const encryptionKey = await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, ['encrypt']) - const iv = await Entropy.create() - const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, encryptionKey, encoded) - const record: SafeRecord = { - iv: iv.hex, - salt: salt.hex, - encrypted - } - return await this.#put(record, name) + return await this.#put(records, store) } catch (err) { throw new Error(this.ERR_MSG) } finally { @@ -113,21 +107,18 @@ export class Safe extends WorkerInterface { /** * Retrieves data from the Safe and decrypts it with a password byte array. */ - static async get (name: string, data: Data): Promise { - const { password } = data - delete data.password - + static async get (store: string, password: ArrayBuffer, name: string): Promise { try { const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) - if (this.#isInvalid(name, derivationKey)) { + if (this.#isInvalid(store, derivationKey)) { throw new Error('Failed to import key') } - const record: SafeRecord = await this.#get(name) + const record: SafeRecord = await this.#get(name, store) if (record == null) { throw new Error('Failed to find record') } - const { encrypted } = record + const { label, encrypted } = record const salt = await Entropy.import(record.salt) const derivationAlgorithm: Pbkdf2Params = { @@ -144,15 +135,9 @@ export class Safe extends WorkerInterface { const iv = await Entropy.import(record.iv) const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, decryptionKey, encrypted) - const decoded = this.#decoder.decode(decrypted) - const deserialized: { [key: string]: string } = JSON.parse(decoded) - const bytes: Data = {} - for (const d of Object.keys(deserialized)) { - bytes[d] = new Uint8Array(base32.toBytes(deserialized[d])).buffer - } - await this.destroy(name) - return bytes + await this.destroy(store, name) + return { [label]: decrypted } } catch (err) { return null } finally { @@ -177,25 +162,34 @@ export class Safe extends WorkerInterface { return false } - static async #exists (name: string): Promise { - return await this.#get(name) !== undefined - } - - static async #delete (name: string): Promise { - try { - const result = await this.#transact('readwrite', db => db.delete(name)) - return !(result || await this.#exists(name)) - } catch { - throw new Error(this.ERR_MSG) - } + static async #delete (store: string, name: string): Promise { + const transaction = this.#storage.transaction(store, 'readwrite') + const db = transaction.objectStore(store) + return new Promise((resolve, reject) => { + const request = db.delete(name) + request.onsuccess = (event) => { + resolve((event.target as IDBRequest).result) + } + request.onerror = (event) => { + console.error('Database error') + reject((event.target as IDBRequest).error) + } + }) } - static async #get (name: string): Promise { - try { - return await this.#transact('readonly', db => db.get(name)) - } catch { - throw new Error('Failed to get record') - } + static async #get (name: string, store: string): Promise { + const transaction = this.#storage.transaction(store, 'readonly') + const db = transaction.objectStore(store) + return new Promise((resolve, reject) => { + const request = db.get(name) + request.onsuccess = (event) => { + resolve((event.target as IDBRequest).result) + } + request.onerror = (event) => { + console.error('Database error') + reject((event.target as IDBRequest).error) + } + }) } static async #open (database: string): Promise { @@ -203,8 +197,10 @@ export class Safe extends WorkerInterface { const request = indexedDB.open(database, 1) request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result - if (!db.objectStoreNames.contains(this.STORE_NAME)) { - db.createObjectStore(this.STORE_NAME) + for (const DB_STORE of this.DB_STORES) { + if (!db.objectStoreNames.contains(DB_STORE)) { + db.createObjectStore(DB_STORE) + } } } request.onsuccess = (event) => { @@ -216,19 +212,15 @@ export class Safe extends WorkerInterface { }) } - static async #put (record: SafeRecord, name: string): Promise { - const result = await this.#transact('readwrite', db => db.put(record, name)) - return await this.#exists(result) - } - - static async #transact (mode: IDBTransactionMode, method: (db: IDBObjectStore) => IDBRequest): Promise { - const db = this.#storage.transaction(this.STORE_NAME, mode).objectStore(this.STORE_NAME) + static async #put (records: SafeRecord[], store: string): Promise { + const transaction = this.#storage.transaction(store, 'readwrite') + const db = transaction.objectStore(store) return new Promise((resolve, reject) => { - const request = method(db) - request.onsuccess = (event) => { - resolve((event.target as IDBRequest).result) + records.map(record => db.put(record, record.label)) + transaction.oncomplete = (event) => { + resolve((event.target as IDBRequest).error == null) } - request.onerror = (event) => { + transaction.onerror = (event) => { console.error('Database error') reject((event.target as IDBRequest).error) } diff --git a/src/types.d.ts b/src/types.d.ts index 4754eb3..81217c2 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -10,14 +10,15 @@ export type Headers = { } export type KeyPair = { - publicKey?: string, - privateKey?: string, + publicKey?: string + privateKey?: string index?: number } export type SafeRecord = { iv: string salt: string + label: string encrypted: ArrayBuffer } diff --git a/test/test.create-wallet.mjs b/test/test.create-wallet.mjs index efceb56..2253f0c 100644 --- a/test/test.create-wallet.mjs +++ b/test/test.create-wallet.mjs @@ -14,11 +14,11 @@ await suite('Create wallets', async () => { await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) - assert.ok(/libnemo_[A-Fa-f0-9]{32,64}/.test(wallet.id)) + assert.ok(/^libnemo_[A-Fa-f0-9]{32,64}$/.test(wallet.id)) assert.ok('mnemonic' in wallet) assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) assert.ok('seed' in wallet) - assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.seed)) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(wallet.seed)) await wallet.destroy() }) @@ -28,11 +28,11 @@ await suite('Create wallets', async () => { await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) assert.ok('id' in wallet) - assert.ok(/libnemo_[A-Fa-f0-9]{32,64}/.test(wallet.id)) + assert.ok(/^libnemo_[A-Fa-f0-9]{32,64}$/.test(wallet.id)) assert.ok('mnemonic' in wallet) assert.ok(/^(?:[a-z]{3,} ){11,23}[a-z]{3,}$/.test(wallet.mnemonic)) assert.ok('seed' in wallet) - assert.ok(/[A-Fa-f0-9]{32,64}/.test(wallet.seed)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(wallet.seed)) await wallet.destroy() }) diff --git a/test/test.derive-accounts.mjs b/test/test.derive-accounts.mjs index 3830780..56dad8b 100644 --- a/test/test.derive-accounts.mjs +++ b/test/test.derive-accounts.mjs @@ -17,7 +17,6 @@ await suite('BIP-44 account derivation', async () => { assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0) assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0) - assert.equals(account.index, 0) const accounts = await wallet.accounts() assert.equals(account, accounts[0]) @@ -46,16 +45,14 @@ await suite('BIP-44 account derivation', async () => { await test('should derive high indexed accounts from the given seed', async () => { const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts(0x70000000, 0x700000ff) + const accounts = await wallet.accounts(0x70000000, 0x7000000f) - assert.equals(accounts.length, 0x100) - for (let i = 0x70000000; i < 0x700000ff; i++) { + assert.equals(accounts.length, 0x10) + for (let i = 0x70000000; i < 0x7000000f; i++) { const a = accounts[i] assert.exists(a) assert.exists(a.address) assert.exists(a.publicKey) - assert.exists(a.index) - assert.equals(a.index, i) const privateKey = await a.exportPrivateKey(wallet.seed, 'hex') assert.exists(privateKey) } @@ -75,19 +72,17 @@ await suite('BLAKE2b account derivation', async () => { assert.exists(a) assert.exists(a.address) assert.exists(a.publicKey) - assert.exists(a.index) const privateKey = await a.exportPrivateKey(wallet.seed, 'hex') assert.exists(privateKey) } - const highAccounts = await wallet.accounts(0x70000000, 0x700000ff) + const highAccounts = await wallet.accounts(0x70000000, 0x7000000f) - assert.equals(highAccounts.length, 0x100) + assert.equals(highAccounts.length, 0x10) for (const a of highAccounts) { assert.exists(a) assert.exists(a.address) assert.exists(a.publicKey) - assert.exists(a.index) const privateKey = await a.exportPrivateKey(wallet.seed, 'hex') assert.exists(privateKey) } diff --git a/test/test.import-wallet.mjs b/test/test.import-wallet.mjs index f0be098..8c9b125 100644 --- a/test/test.import-wallet.mjs +++ b/test/test.import-wallet.mjs @@ -12,16 +12,18 @@ await suite('Import wallets', async () => { await test('nano.org BIP-44 test vector mnemonic', async () => { const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() + const account = await wallet.account() assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) + assert.ok(account instanceof Account) assert.equals(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) assert.equals(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) - assert.equals(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) - assert.equals(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) - assert.equals(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0) + + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0) await wallet.destroy() }) @@ -29,16 +31,18 @@ await suite('Import wallets', async () => { await test('nano.org BIP-44 test vector seed with no mnemonic', async () => { const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() + const account = await wallet.account() assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) - assert.equals(wallet.mnemonic, '') + assert.ok(account instanceof Account) + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) - assert.equals(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) - assert.equals(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) - assert.equals(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + assert.equals(account.publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equals(account.address, NANO_TEST_VECTORS.ADDRESS_0) + + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0) await wallet.destroy() }) @@ -46,60 +50,64 @@ await suite('Import wallets', async () => { await test('Trezor-derived BIP-44 entropy for 12-word mnemonic', async () => { const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_0) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - const account = accounts[0] + const account = await wallet.account() assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_0) assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_0) - assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_0) assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0) assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_0) + await wallet.destroy() }) await test('Trezor-derived BIP-44 entropy for 15-word mnemonic', async () => { const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_1) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - const account = accounts[0] + const account = await wallet.account() assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_1) assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_1) - assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_1) assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1) assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_1) + await wallet.destroy() }) await test('Trezor-derived BIP-44 entropy for 18-word mnemonic', async () => { const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_2) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - const account = accounts[0] + const account = await wallet.account() assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_2) assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_2) - assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_2) assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2) assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_2) + await wallet.destroy() }) await test('Trezor-derived BIP-44 entropy for 21-word mnemonic', async () => { const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_3) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() - const account = accounts[0] + const account = await wallet.account() assert.equals(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_3) assert.equals(wallet.seed, CUSTOM_TEST_VECTORS.SEED_3) - assert.equals(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_3) assert.equals(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3) assert.equals(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, CUSTOM_TEST_VECTORS.PRIVATE_3) + await wallet.destroy() }) @@ -113,12 +121,13 @@ await suite('Import wallets', async () => { assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) assert.equals(wallet.seed, TREZOR_TEST_VECTORS.SEED_0.toUpperCase()) assert.equals(accounts.length, 4) + for (let i = 0; i < accounts.length; i++) { assert.exists(accounts[i]) assert.exists(accounts[i].address) assert.exists(accounts[i].publicKey) - assert.exists(accounts[i].privateKey) - assert.equals(accounts[i].index, i) + const privateKey = await accounts[i].exportPrivateKey(wallet.seed, 'hex') + assert.exists(privateKey) } await wallet.destroy() @@ -139,8 +148,8 @@ await suite('Import wallets', async () => { assert.exists(accounts[i]) assert.exists(accounts[i].address) assert.exists(accounts[i].publicKey) - assert.exists(accounts[i].privateKey) - assert.equals(accounts[i].index, i) + const privateKey = await accounts[i].exportPrivateKey(wallet.seed, 'hex') + assert.exists(privateKey) } await wallet.destroy() @@ -155,16 +164,18 @@ await suite('Import wallets', async () => { assert.ok('seed' in wallet) assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) assert.equals(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + assert.ok(accounts[0] instanceof Account) - assert.equals(accounts[0].index, 0) - assert.equals(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_0) assert.equals(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_0) assert.equals(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_0) + const privateKey0 = await accounts[0].exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey0, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_0) + assert.ok(accounts[1] instanceof Account) - assert.equals(accounts[1].index, 1) - assert.equals(accounts[1].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_1) assert.equals(accounts[1].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_1_PUBLIC_1) assert.equals(accounts[1].address, TREZOR_TEST_VECTORS.BLAKE2B_1_ADDRESS_1) + const privateKey1 = await accounts[1].exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey1, TREZOR_TEST_VECTORS.BLAKE2B_1_PRIVATE_1) await wallet.destroy() }) @@ -172,8 +183,8 @@ await suite('Import wallets', async () => { await test('BLAKE2b seed creates identical wallet as its derived mnemonic', async () => { const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const walletAccounts = await wallet.accounts() - const walletAccount = walletAccounts[0] + const walletAccount = await wallet.account() + const walletAccountPrivateKey = await walletAccount.exportPrivateKey(wallet.seed, 'hex') assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) @@ -182,13 +193,13 @@ await suite('Import wallets', async () => { const imported = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_2) await imported.unlock(TREZOR_TEST_VECTORS.PASSWORD) - const importedAccounts = await imported.accounts() - const importedAccount = importedAccounts[0] + const importedAccount = await imported.account() + const importedAccountPrivateKey = await importedAccount.exportPrivateKey(imported.seed, 'hex') assert.equals(imported.mnemonic, wallet.mnemonic) assert.equals(imported.seed, wallet.seed) - assert.equals(importedAccount.privateKey, walletAccount.privateKey) assert.equals(importedAccount.publicKey, walletAccount.publicKey) + assert.equals(importedAccountPrivateKey, walletAccountPrivateKey) await wallet.destroy() }) @@ -196,17 +207,18 @@ await suite('Import wallets', async () => { await test('BLAKE2b mnemonic for maximum seed value', async () => { const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) - const accounts = await wallet.accounts() + const account = await wallet.account() assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.ok(accounts[0] instanceof Account) + assert.ok(account instanceof Account) assert.equals(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_3) assert.equals(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_3) - assert.equals(accounts[0].index, 0) - assert.equals(accounts[0].privateKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PRIVATE_0) - assert.equals(accounts[0].publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0) - assert.equals(accounts[0].address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0) + assert.equals(account.publicKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PUBLIC_0) + assert.equals(account.address, TREZOR_TEST_VECTORS.BLAKE2B_3_ADDRESS_0) + + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') + assert.equals(privateKey, TREZOR_TEST_VECTORS.BLAKE2B_3_PRIVATE_0) await wallet.destroy() }) @@ -240,7 +252,7 @@ await suite('Retrieve wallets from session storage using a wallet-generated ID', assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) @@ -260,7 +272,7 @@ await suite('Retrieve wallets from session storage using a wallet-generated ID', assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) diff --git a/test/test.ledger.mjs b/test/test.ledger.mjs index a406eb1..6a1b4d0 100644 --- a/test/test.ledger.mjs +++ b/test/test.ledger.mjs @@ -103,19 +103,19 @@ await suite('Ledger hardware wallet', { skip: true || isNode }, async () => { '0' ) - assert.ok(/[A-Fa-f0-9]{64}/.test(openBlock.hash)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(openBlock.hash)) assert.nullish(openBlock.signature) assert.equals(openBlock.account.publicKey, account.publicKey) const { status, hash, signature } = await wallet.sign(0, openBlock) assert.equals(status, 'OK') - assert.ok(/[A-Fa-f0-9]{64}/.test(hash)) - assert.ok(/[A-Fa-f0-9]{128}/.test(signature)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash)) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature)) await openBlock.sign(0) - assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature)) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(openBlock.signature)) assert.equals(signature, openBlock.signature) }) @@ -135,15 +135,15 @@ await suite('Ledger hardware wallet', { skip: true || isNode }, async () => { openBlock.hash ) - assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(sendBlock.hash)) assert.nullish(sendBlock.signature) assert.equals(sendBlock.account.publicKey, account.publicKey) const { status, hash, signature } = await wallet.sign(0, sendBlock) assert.equals(status, 'OK') - assert.ok(/[A-Fa-f0-9]{64}/.test(hash)) - assert.ok(/[A-Fa-f0-9]{128}/.test(signature)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(hash)) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(signature)) sendBlock.signature = signature }) @@ -157,13 +157,13 @@ await suite('Ledger hardware wallet', { skip: true || isNode }, async () => { sendBlock.hash ) - assert.ok(/[A-Fa-f0-9]{64}/.test(sendBlock.hash)) + assert.ok(/^[A-Fa-f0-9]{64}$/.test(sendBlock.hash)) assert.nullish(receiveBlock.signature) assert.equals(receiveBlock.account.publicKey, account.publicKey) await receiveBlock.sign(0, sendBlock) - assert.ok(/[A-Fa-f0-9]{128}/.test(receiveBlock.signature)) + assert.ok(/^[A-Fa-f0-9]{128}$/.test(receiveBlock.signature)) }) // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14 @@ -172,7 +172,7 @@ await suite('Ledger hardware wallet', { skip: true || isNode }, async () => { const { status, signature } = await click('Click to sign nonce', wallet.sign(0, nonce)) assert.equals(status, 'OK') - assert.OK(/[A-Fa-f0-9]{128}/.test(signature)) + assert.OK(/^[A-Fa-f0-9]{128}$/.test(signature)) }) await test('destroy wallet', async () => { diff --git a/test/test.lock-unlock.mjs b/test/test.lock-unlock.mjs index f011431..06804a3 100644 --- a/test/test.lock-unlock.mjs +++ b/test/test.lock-unlock.mjs @@ -14,7 +14,7 @@ await suite('Lock and unlock wallets', async () => { assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) @@ -37,7 +37,7 @@ await suite('Lock and unlock wallets', async () => { assert.ok(lockResult) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(new Uint8Array(key)) @@ -55,19 +55,15 @@ await suite('Lock and unlock wallets', async () => { const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - const lockResult = await account.lock(NANO_TEST_VECTORS.PASSWORD) + const lockResult = await wallet.lock(NANO_TEST_VECTORS.PASSWORD) assert.equals(lockResult, true) - assert.ok(account.isLocked) - assert.ok('privateKey' in account) - assert.equals(account.privateKey, '') - const unlockResult = await account.unlock(NANO_TEST_VECTORS.PASSWORD) + const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') assert.equals(unlockResult, true) - assert.ok(account.isUnlocked) - assert.ok('privateKey' in account) - assert.equals(account.privateKey, NANO_TEST_VECTORS.PRIVATE_0) + assert.equals(privateKey, NANO_TEST_VECTORS.PRIVATE_0) await wallet.destroy() }) @@ -81,7 +77,7 @@ await suite('Lock and unlock wallets', async () => { assert.equals(lockResult, true) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) await wallet.destroy() @@ -98,7 +94,7 @@ await suite('Lock and unlock wallets', async () => { assert.equals(lockResult, true) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) await wallet.destroy() @@ -111,7 +107,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(new Uint8Array(key)), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) await wallet.destroy() @@ -132,7 +128,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) await wallet.destroy() @@ -153,7 +149,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) await wallet.destroy() @@ -164,7 +160,7 @@ await suite('Lock and unlock wallets', async () => { assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) @@ -187,7 +183,7 @@ await suite('Lock and unlock wallets', async () => { assert.equals(lockResult, true) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.equals(wallet.mnemonic, '') + assert.nullish(wallet.mnemonic) assert.equals(wallet.seed, '') const unlockResult = await wallet.unlock(new Uint8Array(key)) @@ -206,19 +202,15 @@ await suite('Lock and unlock wallets', async () => { const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() - const lockResult = await account.lock(NANO_TEST_VECTORS.PASSWORD) + const lockResult = await wallet.lock(NANO_TEST_VECTORS.PASSWORD) assert.equals(lockResult, true) - assert.ok(account.isLocked) - assert.ok('privateKey' in account) - assert.equals(account.privateKey, '') - const unlockResult = await account.unlock(NANO_TEST_VECTORS.PASSWORD) + const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const privateKey = await account.exportPrivateKey(wallet.seed, 'hex') assert.equals(unlockResult, true) - assert.ok(account.isUnlocked) - assert.ok('privateKey' in account) - assert.equals(account.privateKey, TREZOR_TEST_VECTORS.BLAKE2B_PRIVATE_0) + assert.equals(privateKey, TREZOR_TEST_VECTORS.BLAKE2B_PRIVATE_0) await wallet.destroy() }) @@ -229,7 +221,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) await wallet.destroy() @@ -246,7 +238,7 @@ await suite('Lock and unlock wallets', async () => { assert.equals(lockResult, true) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) await wallet.destroy() @@ -259,7 +251,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(new Uint8Array(key)), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) await wallet.destroy() @@ -280,7 +272,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) await wallet.destroy() @@ -301,7 +293,7 @@ await suite('Lock and unlock wallets', async () => { await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' }) assert.ok('mnemonic' in wallet) assert.ok('seed' in wallet) - assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.nullish(wallet.mnemonic) assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) await wallet.destroy() diff --git a/test/test.main.mjs b/test/test.main.mjs index 90f7644..2401b56 100644 --- a/test/test.main.mjs +++ b/test/test.main.mjs @@ -1,16 +1,16 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later -import './test.blake2b.mjs' -import './test.calculate-pow.mjs' -import './test.create-wallet.mjs' -import './test.derive-accounts.mjs' -import './test.import-wallet.mjs' -import './test.ledger.mjs' -import './test.lock-unlock.mjs' -import './test.manage-rolodex.mjs' -import './test.refresh-accounts.mjs' -import './test.sign-blocks.mjs' +// import './test.blake2b.mjs' +// import './test.calculate-pow.mjs' +// import './test.create-wallet.mjs' +// import './test.derive-accounts.mjs' +// import './test.import-wallet.mjs' +// import './test.ledger.mjs' +// import './test.lock-unlock.mjs' +// import './test.manage-rolodex.mjs' +// import './test.refresh-accounts.mjs' +// import './test.sign-blocks.mjs' import './test.tools.mjs' console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') diff --git a/test/test.tools.mjs b/test/test.tools.mjs index bff22f7..695f5b4 100644 --- a/test/test.tools.mjs +++ b/test/test.tools.mjs @@ -105,6 +105,7 @@ await suite('signature tests', async () => { const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() + const privateKey = await account.exportPrivateKey(wallet.seed) const sendBlock = new SendBlock( account.address, '5618869000000000000000000000000', @@ -113,7 +114,7 @@ await suite('signature tests', async () => { 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', ) - await sendBlock.sign(account.privateKey ?? '') + await sendBlock.sign(privateKey) const valid = await sendBlock.verify(account.publicKey) assert.equals(valid, true) @@ -124,6 +125,7 @@ await suite('signature tests', async () => { const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) const account = await wallet.account() + const privateKey = await account.exportPrivateKey(wallet.seed) const sendBlock = new SendBlock( account.address, '5618869000000000000000000000000', @@ -132,9 +134,9 @@ await suite('signature tests', async () => { 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', ) - await sendBlock.sign(account.privateKey ?? '') + await sendBlock.sign(privateKey) - sendBlock.account = Account.fromAddress('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p') + sendBlock.account = Account.import('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p') const valid = await sendBlock.verify(account.publicKey) assert.equals(valid, false)