From d5c5ff2bfeacd367188b59be59f6beeadcdc5a9a Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 31 Jul 2025 23:37:50 -0700 Subject: [PATCH] Successfully signed open block with wallet. --- src/lib/database.ts | 8 ++-- src/lib/safe.ts | 31 ++++++++++++---- src/lib/wallet.ts | 87 +++++++++++++++++++++++++++++++++++--------- src/types.d.ts | 20 +++++++++- test/main.test.mjs | 20 +++++----- test/test.blocks.mjs | 2 +- 6 files changed, 127 insertions(+), 41 deletions(-) diff --git a/src/lib/database.ts b/src/lib/database.ts index 959c16c..529c77a 100644 --- a/src/lib/database.ts +++ b/src/lib/database.ts @@ -79,7 +79,7 @@ export class Database { transaction.oncomplete = (event) => { const results: NamedData = {} for (const request of requests) { - results[request.result.id] = request.error ?? request.result + results[request.result.name] = request.error ?? request.result } resolve(results) } @@ -109,7 +109,7 @@ export class Database { } else { const results: NamedData = {} for (const result of request.result) { - results[result.id] = request.error ?? result[result.id] + results[result.id] = request.error ?? result } resolve(results) } @@ -132,7 +132,7 @@ export class Database { const transaction = this.#storage.transaction(store, 'readwrite') const db = transaction.objectStore(store) return new Promise((resolve, reject) => { - const requests = Object.keys(data).map(key => db.put({ id: key, [key]: data[key] })) + const requests = Object.keys(data).map(key => db.put(data[key], key)) transaction.oncomplete = (event) => { const results = [] for (const request of requests) { @@ -156,7 +156,7 @@ export class Database { } for (const DB_STORE of this.DB_STORES) { if (!db.objectStoreNames.contains(DB_STORE)) { - db.createObjectStore(DB_STORE, { keyPath: 'id' }) + db.createObjectStore(DB_STORE) } } } diff --git a/src/lib/safe.ts b/src/lib/safe.ts index 1defc8e..41fdadc 100644 --- a/src/lib/safe.ts +++ b/src/lib/safe.ts @@ -4,13 +4,14 @@ 'use strict' import { parentPort } from 'node:worker_threads' +import { Bip39Mnemonic } from './bip39-mnemonic.js' import { Bip39Words } from './bip39-wordlist' import { Bip44Ckd } from './bip44-ckd' import { Blake2b } from './blake2b' import { Blake2bCkd } from './blake2b-ckd' -import { NanoNaCl } from './nano-nacl' -import { Bip39Mnemonic } from './bip39-mnemonic.js' +import { BIP39_ITERATIONS } from './constants' import { default as Convert, bytes, hex, utf8 } from './convert.js' +import { NanoNaCl } from './nano-nacl' import { NamedData } from '#types' /** @@ -34,6 +35,7 @@ export class Safe { type, key, keySalt, + iv, seed, mnemonicPhrase, mnemonicSalt, @@ -69,7 +71,7 @@ export class Safe { break } case 'unlock': { - result = await this.unlock(key, keySalt, encrypted) + result = await this.unlock(key, iv, encrypted) break } case 'verify': { @@ -86,13 +88,15 @@ export class Safe { transfer.push(result[k]) } } + debugger //@ts-expect-error BROWSER: postMessage(result, transfer) //@ts-expect-error NODE: parentPort?.postMessage(result, transfer) } catch (err) { - BROWSER: postMessage({ error: 'Failed to derive key from password', cause: err }) - NODE: parentPort?.postMessage({ error: 'Failed to derive key from password', cause: err }) + console.error(err) + BROWSER: postMessage({ error: 'Failed to process Safe request', cause: err }) + NODE: parentPort?.postMessage({ error: 'Failed to process Safe request', cause: err }) } } BROWSER: addEventListener('message', listener) @@ -248,10 +252,11 @@ export class Safe { throw new TypeError('Invalid seed') } this.#seed = seed - this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic) + if (mnemonic != null) this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic) this.#locked = false return { isUnlocked: !this.#locked } } catch (err) { + console.error(err) throw new Error('Failed to unlock wallet', { cause: err }) } } @@ -301,6 +306,8 @@ export class Safe { } static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, keySalt: ArrayBuffer): Promise { + console.log(keySalt) + debugger const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey']) new Uint8Array(password).fill(0).buffer.transfer() const derivationAlgorithm: Pbkdf2Params = { @@ -317,6 +324,8 @@ export class Safe { } static async #decryptWallet (key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise> { + console.log(iv, encrypted) + debugger const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encrypted) const decoded = JSON.parse(bytes.toUtf8(new Uint8Array(decrypted))) const seed = hex.toBuffer(decoded.seed) @@ -366,7 +375,7 @@ export class Safe { throw new TypeError('Invalid wallet action') } const action = messageData.action - debugger + // Password for lock/unlock key if (messageData.password != null && !(messageData.password instanceof ArrayBuffer)) { throw new TypeError('Password must be ArrayBuffer') @@ -400,6 +409,11 @@ export class Safe { } const type: 'BIP-44' | 'BLAKE2b' | undefined = messageData.type + // Import requires seed or mnemonic phrase + if (action === 'import' && messageData.seed == null && messageData.mnemonicPhrase == null) { + throw new TypeError('Seed or mnemonic phrase required to import wallet') + } + // Seed to import if (action === 'import' && !(messageData.seed instanceof ArrayBuffer)) { throw new TypeError('Seed required to import wallet') @@ -409,7 +423,7 @@ export class Safe { : undefined // Mnemonic phrase to import - if (action === 'import' && typeof messageData.mnemonicPhrase !== 'string') { + if (action === 'import' && 'mnemonicPhrase' in message && typeof messageData.mnemonicPhrase !== 'string') { throw new TypeError('Invalid mnemonic phrase') } const mnemonicPhrase = typeof messageData.mnemonicPhrase === 'string' @@ -467,6 +481,7 @@ NODE: importWorkerThreads = `import { parentPort } from 'node:worker_threads'` export default ` ${importWorkerThreads} ${Convert} + const BIP39_ITERATIONS = ${BIP39_ITERATIONS} const Bip39Mnemonic = ${Bip39Mnemonic} const Bip39Words = ["${Bip39Words.join('","')}"] const Bip44Ckd = ${Bip44Ckd} diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts index 94b6a23..02810b5 100644 --- a/src/lib/wallet.ts +++ b/src/lib/wallet.ts @@ -9,7 +9,7 @@ import { Database } from '#src/lib/database.js' import { Rpc } from '#src/lib/rpc.js' import { default as SafeWorker } from '#src/lib/safe.js' import { WorkerQueue } from '#src/lib/worker-queue.js' -import { KeyPair, WalletType } from '#types' +import { KeyPair, NamedData, WalletType } from '#types' /** * Represents a wallet containing numerous Nano accounts derived from a single @@ -25,13 +25,8 @@ export class Wallet { */ static async #get (name: string) { try { - const record = await Database.get(name, this.#DB_NAME) - const decoded = JSON.parse(record[name]) - const type: 'BIP-44' | 'BLAKE2b' = decoded.type - const iv: ArrayBuffer = hex.toBuffer(decoded.iv) - const salt: ArrayBuffer = hex.toBuffer(decoded.salt) - const encrypted: ArrayBuffer = hex.toBuffer(decoded.encrypted) - return { type, iv, salt, encrypted } + const record = await Database.get(name, this.#DB_NAME) + return record[name] } catch (err) { throw new Error('Failed to get wallet from database', { cause: err }) } @@ -49,19 +44,71 @@ export class Wallet { Wallet.#isInternal = true const self = new this(name, type) try { + debugger const { iv, salt, encrypted } = await self.#safe.request({ action: 'create', type, password: utf8.toBuffer(password), mnemonicSalt: mnemonicSalt ?? '' }) - const encoded = JSON.stringify({ + const data = { + name, type, - iv: bytes.toHex(new Uint8Array(iv)), - salt: bytes.toHex(new Uint8Array(salt)), - encrypted: bytes.toHex(new Uint8Array(encrypted)) - }) - await Database.put({ [name]: encoded }, Wallet.#DB_NAME) + iv, + salt, + encrypted + } + await Database.put({ [name]: data }, Wallet.#DB_NAME) + return self + } catch (err) { + throw new Error('Error creating new Wallet', { cause: err }) + } + } + + /** + * Imports an existing HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Wallet} A newly instantiated Wallet + */ + static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise + /** + * Imports an existing HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Wallet} A newly instantiated Wallet + */ + static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise + static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise { + Wallet.#isInternal = true + const self = new this(name, type) + try { + const data: any = { + action: 'import', + type, + password: utf8.toBuffer(password), + mnemonicSalt + } + if (/^(?:[A-Fa-f0-9]{64}){1,2}$/.test(secret)) { + data.seed = hex.toBuffer(secret) + } else { + data.mnemonicPhrase = secret + } + const result = self.#safe.request(data) + const { iv, salt, encrypted } = await result + const record = { + name, + type, + iv, + salt, + encrypted + } + console.log(record) + await Database.put({ [name]: record }, Wallet.#DB_NAME) return self } catch (err) { throw new Error('Error creating new Wallet', { cause: err }) @@ -80,6 +127,9 @@ export class Wallet { throw new TypeError('Wallet name is required to restore') } const { type } = await this.#get(name) + if (type !== 'BIP-44' && type !== 'BLAKE2b') { + throw new Error('Invalid wallet type from database') + } Wallet.#isInternal = true return new this(name, type) } catch (err) { @@ -261,7 +311,7 @@ export class Wallet { const { signature } = await this.#safe.request({ action: 'sign', index, - data: JSON.stringify(block) + data: hex.toBuffer(block.hash) }) const sig = bytes.toHex(new Uint8Array(signature)) block.signature = sig @@ -281,14 +331,17 @@ export class Wallet { */ async unlock (password: string): Promise { try { + debugger const { iv, salt, encrypted } = await Wallet.#get(this.#name) - const { isUnlocked } = await this.#safe.request({ + console.log(iv, salt, encrypted) + const result = await this.#safe.request({ action: 'unlock', password: utf8.toBuffer(password), iv, - salt, + keySalt: salt, encrypted }) + const { isUnlocked } = result if (!isUnlocked) { throw new Error('Unlock request to Safe failed') } diff --git a/src/types.d.ts b/src/types.d.ts index 2950732..4261cb0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -377,7 +377,7 @@ export declare class ChangeBlock extends Block { constructor (account: Account | string, balance: string, representative: Account | string, frontier: string, work?: string) } -export type Data = boolean | number | number[] | string | string[] | ArrayBuffer | CryptoKey +export type Data = boolean | number | number[] | string | string[] | ArrayBuffer | CryptoKey | { [key: string]: Data } /** * Represents a cryptographically strong source of entropy suitable for use in @@ -602,6 +602,24 @@ export declare class Wallet { */ static create (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicSalt?: string): Promise /** + * Imports an existing HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Wallet} A newly instantiated Wallet + */ + static import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise + /** + * Imports an existing HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Wallet} A newly instantiated Wallet + */ + static import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise + /** * Retrieves an existing wallet from the database using its name. * * @param {string} name - Entered by user when the wallet was initially created diff --git a/test/main.test.mjs b/test/main.test.mjs index ada0c28..3ab48d6 100644 --- a/test/main.test.mjs +++ b/test/main.test.mjs @@ -4,17 +4,17 @@ import { failures, passes } from './GLOBALS.mjs' import './test.runner-check.mjs' -import './test.blake2b.mjs' +// import './test.blake2b.mjs' import './test.blocks.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.tools.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.tools.mjs' console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') console.log('%cPASS: ', 'color:green;font-weight:bold', passes.length) diff --git a/test/test.blocks.mjs b/test/test.blocks.mjs index 5e7ee8c..6347a64 100644 --- a/test/test.blocks.mjs +++ b/test/test.blocks.mjs @@ -85,7 +85,7 @@ await Promise.all([ suite('Block signing using official test vectors', async () => { await test('sign open block with wallet', async () => { - const wallet = await Wallet.create('Test', 'BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + const wallet = await Wallet.import('Test', 'BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) await assert.resolves(wallet.unlock(NANO_TEST_VECTORS.PASSWORD)) const block = new ReceiveBlock( -- 2.47.3