]> git.codecow.com Git - libnemo.git/commitdiff
Successfully signed open block with wallet.
authorChris Duncan <chris@zoso.dev>
Fri, 1 Aug 2025 06:37:50 +0000 (23:37 -0700)
committerChris Duncan <chris@zoso.dev>
Fri, 1 Aug 2025 06:37:50 +0000 (23:37 -0700)
src/lib/database.ts
src/lib/safe.ts
src/lib/wallet.ts
src/types.d.ts
test/main.test.mjs
test/test.blocks.mjs

index 959c16c6538e26ab11636530aac6e96004ae62a0..529c77acbda21064ae564665d5d5c327171df6b4 100644 (file)
@@ -79,7 +79,7 @@ export class Database {
                        transaction.oncomplete = (event) => {
                                const results: NamedData<T> = {}
                                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<T> = {}
                                        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)
                                        }
                                }
                        }
index 1defc8e3c4dbed5041779b408f78206fb3eb1dd1..41fdadc8b079ca47e43a42e1db83cde639801db6 100644 (file)
@@ -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<CryptoKey> {
+               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<NamedData<string | ArrayBuffer>> {
+               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}
index 94b6a23af58d4de280daee383b2269018178219f..02810b50e5f2037e3ddf70f1efef36f67441bbc2 100644 (file)
@@ -9,7 +9,7 @@ import { Database } from '#src/lib/database.js'
 import { Rpc } from '#src/lib/rpc.js'\r
 import { default as SafeWorker } from '#src/lib/safe.js'\r
 import { WorkerQueue } from '#src/lib/worker-queue.js'\r
-import { KeyPair, WalletType } from '#types'\r
+import { KeyPair, NamedData, WalletType } from '#types'\r
 \r
 /**\r
 * Represents a wallet containing numerous Nano accounts derived from a single\r
@@ -25,13 +25,8 @@ export class Wallet {
        */\r
        static async #get (name: string) {\r
                try {\r
-                       const record = await Database.get<string>(name, this.#DB_NAME)\r
-                       const decoded = JSON.parse(record[name])\r
-                       const type: 'BIP-44' | 'BLAKE2b' = decoded.type\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
-                       return { type, iv, salt, encrypted }\r
+                       const record = await Database.get<NamedData>(name, this.#DB_NAME)\r
+                       return record[name]\r
                } catch (err) {\r
                        throw new Error('Failed to get wallet from database', { cause: err })\r
                }\r
@@ -49,19 +44,71 @@ export class Wallet {
                Wallet.#isInternal = true\r
                const self = new this(name, type)\r
                try {\r
+                       debugger\r
                        const { iv, salt, encrypted } = await self.#safe.request<ArrayBuffer>({\r
                                action: 'create',\r
                                type,\r
                                password: utf8.toBuffer(password),\r
                                mnemonicSalt: mnemonicSalt ?? ''\r
                        })\r
-                       const encoded = JSON.stringify({\r
+                       const data = {\r
+                               name,\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 }, Wallet.#DB_NAME)\r
+                               iv,\r
+                               salt,\r
+                               encrypted\r
+                       }\r
+                       await Database.put({ [name]: data }, Wallet.#DB_NAME)\r
+                       return self\r
+               } catch (err) {\r
+                       throw new Error('Error creating new Wallet', { cause: err })\r
+               }\r
+       }\r
+\r
+       /**\r
+       * Imports an existing 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 {Wallet} A newly instantiated Wallet\r
+       */\r
+       static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, seed: string): Promise<Wallet>\r
+       /**\r
+       * Imports an existing 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 {Wallet} A newly instantiated Wallet\r
+       */\r
+       static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, mnemonicPhrase: string, mnemonicSalt?: string): Promise<Wallet>\r
+       static async import (name: string, type: 'BIP-44' | 'BLAKE2b', password: string, secret: string, mnemonicSalt?: string): Promise<Wallet> {\r
+               Wallet.#isInternal = true\r
+               const self = new this(name, type)\r
+               try {\r
+                       const data: any = {\r
+                               action: 'import',\r
+                               type,\r
+                               password: utf8.toBuffer(password),\r
+                               mnemonicSalt\r
+                       }\r
+                       if (/^(?:[A-Fa-f0-9]{64}){1,2}$/.test(secret)) {\r
+                               data.seed = hex.toBuffer(secret)\r
+                       } else {\r
+                               data.mnemonicPhrase = secret\r
+                       }\r
+                       const result = self.#safe.request<ArrayBuffer>(data)\r
+                       const { iv, salt, encrypted } = await result\r
+                       const record = {\r
+                               name,\r
+                               type,\r
+                               iv,\r
+                               salt,\r
+                               encrypted\r
+                       }\r
+                       console.log(record)\r
+                       await Database.put({ [name]: record }, Wallet.#DB_NAME)\r
                        return self\r
                } catch (err) {\r
                        throw new Error('Error creating new Wallet', { cause: err })\r
@@ -80,6 +127,9 @@ export class Wallet {
                                throw new TypeError('Wallet name is required to restore')\r
                        }\r
                        const { type } = await this.#get(name)\r
+                       if (type !== 'BIP-44' && type !== 'BLAKE2b') {\r
+                               throw new Error('Invalid wallet type from database')\r
+                       }\r
                        Wallet.#isInternal = true\r
                        return new this(name, type)\r
                } catch (err) {\r
@@ -261,7 +311,7 @@ export class Wallet {
                        const { signature } = await this.#safe.request<ArrayBuffer>({\r
                                action: 'sign',\r
                                index,\r
-                               data: JSON.stringify(block)\r
+                               data: hex.toBuffer(block.hash)\r
                        })\r
                        const sig = bytes.toHex(new Uint8Array(signature))\r
                        block.signature = sig\r
@@ -281,14 +331,17 @@ export class Wallet {
        */\r
        async unlock (password: string): Promise<boolean> {\r
                try {\r
+                       debugger\r
                        const { iv, salt, encrypted } = await Wallet.#get(this.#name)\r
-                       const { isUnlocked } = await this.#safe.request<boolean>({\r
+                       console.log(iv, salt, encrypted)\r
+                       const result = await this.#safe.request<boolean>({\r
                                action: 'unlock',\r
                                password: utf8.toBuffer(password),\r
                                iv,\r
-                               salt,\r
+                               keySalt: salt,\r
                                encrypted\r
                        })\r
+                       const { isUnlocked } = result\r
                        if (!isUnlocked) {\r
                                throw new Error('Unlock request to Safe failed')\r
                        }\r
index 29507325b00ffe2b2b4a57cbfd922668669f0e49..4261cb07e599d9a2c5cfbd4b1e5df498aca3ce36 100644 (file)
@@ -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<Wallet>
        /**
+       * 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<Wallet>
+       /**
+       * 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<Wallet>
+       /**
        * Retrieves an existing wallet from the database using its name.
        *
        * @param {string} name - Entered by user when the wallet was initially created
index ada0c28a6a82c56c51ec92ec5f2b578718caf2c9..3ab48d6ad4c1eacdfa59c884afa7e240ce61e552 100644 (file)
@@ -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)
index 5e7ee8c0368e074dedcced919ddecd2c976c9c88..6347a6474161b1de307171f6c964e2688643c410 100644 (file)
@@ -85,7 +85,7 @@ await Promise.all([
        suite('Block signing using official test vectors', async () => {\r
 \r
                await test('sign open block with wallet', async () => {\r
-                       const wallet = await Wallet.create('Test', 'BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
+                       const wallet = await Wallet.import('Test', 'BIP-44', NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
                        await assert.resolves(wallet.unlock(NANO_TEST_VECTORS.PASSWORD))\r
 \r
                        const block = new ReceiveBlock(\r