]> git.codecow.com Git - libnemo.git/commitdiff
Save rolodex data insecurely by using same empty password when storing and fetching...
authorChris Duncan <chris@zoso.dev>
Mon, 21 Jul 2025 06:21:45 +0000 (23:21 -0700)
committerChris Duncan <chris@zoso.dev>
Mon, 21 Jul 2025 06:21:45 +0000 (23:21 -0700)
src/lib/account.ts
src/lib/rolodex.ts
src/lib/workers/safe.ts

index 28cae5642f3ebd40ea104ed7068e61c73b5dd9a7..a3abc10673688ee41932ef5c8f56d4edfe9d8e5c 100644 (file)
@@ -385,8 +385,8 @@ export class Account {
                }\r
 \r
                try {\r
-                       const isLocked = await SafeWorker.assign(privateAccounts)\r
-                       if (!isLocked) {\r
+                       const { result } = await SafeWorker.assign(privateAccounts)\r
+                       if (!result) {\r
                                throw null\r
                        }\r
                        return accounts\r
index 2d1a886aae9812a900ca73a163f5ff923d2ea37a..73df66f5e3267f12160f0a391dbc8d6f8f351571 100644 (file)
@@ -2,7 +2,9 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import { Account } from './account'
+import { bytes, utf8 } from './convert'
 import { RolodexEntry } from '#types'
+import { SafeWorker } from '#workers'
 
 /**
 * Represents a basic address book of Nano accounts. Multiple addresses can be
@@ -16,18 +18,21 @@ export class Rolodex {
        *
        * If the name exists, add the address as a new association to that name. If
        * the account exists under a different name, update the name. If no matches
-       * are found at all, add a completely new entry.
+       * are found at all, add a new entry.
        *
        * @param {string} name - Alias for the address
        * @param {string} address - Nano account address
        */
-       async add (name: string, address: string): Promise<void> {
+       async add (name: string, address: string): Promise<boolean> {
                if (name == null || name === '') {
                        throw new Error('Name is required for rolodex entries')
                }
                if (typeof name !== 'string') {
                        throw new Error('Name must be a string for rolodex entries')
                }
+               if (name.slice(0, 5) === 'nano_' || name.slice(0, 4) === 'xrb_') {
+                       throw new Error('Name cannot start with an address prefix')
+               }
                if (address == null || address === '') {
                        throw new Error('Address is required for rolodex entries')
                }
@@ -40,44 +45,95 @@ export class Rolodex {
                        .replaceAll('>', '\\u003d')
                        .replaceAll('\\', '\\u005c')
                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) {
-                       this.#entries.push({ name, account })
-               } else if (!nameResult) {
-                       accountResult.name = name
+
+               const nameResult = await this.getName(account.address)
+               if (nameResult == null) {
+                       const { result } = await SafeWorker.assign<boolean>({
+                               method: 'store',
+                               store: 'Rolodex',
+                               password: '',
+                               [address]: utf8.toBytes(name).buffer
+                       })
+                       return result
                }
+
+               const addresses = await this.getAddresses(name)
+               if (addresses.length > 0) {
+                       addresses.push(account.address)
+                       const addressesJson = JSON.stringify(addresses)
+                       const { result } = await SafeWorker.assign<boolean>({
+                               method: 'store',
+                               store: 'Rolodex',
+                               password: '',
+                               [name]: utf8.toBytes(addressesJson).buffer
+                       })
+                       return result
+               }
+
+               return false
        }
 
        /**
        * Gets the name associated with a specific Nano address from the rolodex.
        *
        * @param {string} address - Nano account address
-       * @returns {string|null} Name associated with the address, or null if not found
+       * @returns {Promise<string|null>} Promise for the name associated with the address, or null if not found
        */
-       getName (address: string): string | null {
-               const result = this.#entries.find(e => e.account.address === address)
-               return result?.name ?? null
+       async getName (address: string): Promise<string | null> {
+               try {
+                       const response = await SafeWorker.assign<ArrayBuffer>({
+                               method: 'fetch',
+                               name: address,
+                               store: 'Rolodex',
+                               password: ''
+                       })
+                       return bytes.toUtf8(new Uint8Array(response[address]))
+               } catch (err) {
+                       console.log(err)
+                       return null
+               }
        }
 
        /**
        * Gets all Nano addresses associated with a name from the rolodex.
        *
        * @param {string} name - Alias to look up
-       * @returns {string[]} List of Nano addresses associated with the name
+       * @returns {Promise<string[]>} Promise for a list of Nano addresses associated with the name
        */
-       getAddresses (name: string): string[] {
-               const entries = this.#entries.filter(e => e.name === name)
-               return entries.map(a => a.account.address)
+       async getAddresses (name: string): Promise<string[]> {
+               try {
+                       const response = await SafeWorker.assign<ArrayBuffer>({
+                               method: 'fetch',
+                               name,
+                               store: 'Rolodex',
+                               password: ''
+                       })
+                       const addressesJson = bytes.toUtf8(new Uint8Array(response[name]))
+                       return JSON.parse(addressesJson)
+               } catch (err) {
+                       console.log(err)
+                       return []
+               }
        }
 
        /**
        * Gets all names stored in the rolodex.
        *
-       * @returns {string[]} List of names stored in the rolodex
+       * @returns {Promise<string[]>} Promise for a list of all names stored in the rolodex
        */
-       getAllNames (): string[] {
-               return this.#entries.map(e => e.name)
+       async getAllNames (): Promise<string[]> {
+               try {
+                       const response = await SafeWorker.assign<ArrayBuffer>({
+                               method: 'export',
+                               name: '',
+                               store: 'Rolodex',
+                               password: ''
+                       })
+                       return Object.keys(response).filter(v => v.slice(0, 5) !== 'nano_')
+               } catch (err) {
+                       console.log(err)
+                       return []
+               }
        }
 
        /**
index 4df02969a22b61c923374400db681e74dab1c3bf..725168841eb8ed28a266b20ff98c6193bf0169e8 100644 (file)
@@ -47,6 +47,9 @@ export class Safe extends WorkerInterface {
                                case 'fetch': {
                                        return await this.fetch(name, store, password)
                                }
+                               case 'export': {
+                                       return await this.fetch(name, store, password, true)
+                               }
                                case 'destroy': {
                                        return { result: await this.destroy(name, store) }
                                }
@@ -110,7 +113,7 @@ export class Safe extends WorkerInterface {
        /**
        * Retrieves data from the Safe and decrypts it with a password byte array.
        */
-       static async fetch (name: string | string[] | unknown, store: string | unknown, password: ArrayBuffer | unknown): Promise<NamedData<ArrayBuffer>> {
+       static async fetch (name: string | string[] | unknown, store: string | unknown, password: ArrayBuffer | unknown, all: boolean = false): Promise<NamedData<ArrayBuffer>> {
                const names = Array.isArray(name) ? name : [name]
                if (names.some(v => typeof v !== 'string')) {
                        throw new Error('Invalid fields')
@@ -125,7 +128,9 @@ export class Safe extends WorkerInterface {
 
                const results: NamedData<ArrayBuffer> = {}
                try {
-                       const records: SafeRecord[] = await this.#get(fields, store)
+                       const records: SafeRecord[] = all
+                               ? await this.#getAll(store)
+                               : await this.#get(fields, store)
                        if (records == null || records.length === 0) {
                                throw new Error('Failed to find records')
                        }
@@ -176,11 +181,11 @@ export class Safe extends WorkerInterface {
                })
        }
 
-       static async #get (fields: string[], store: string): Promise<SafeRecord[]> {
+       static async #get (names: string[], store: string): Promise<SafeRecord[]> {
                const transaction = this.#storage.transaction(store, 'readonly')
                const db = transaction.objectStore(store)
                return new Promise((resolve, reject) => {
-                       const requests = fields.map(field => db.get(field))
+                       const requests = names.map(name => db.get(name))
                        transaction.oncomplete = (event) => {
                                const results = requests.map(r => r.result)
                                resolve(results)
@@ -192,6 +197,21 @@ export class Safe extends WorkerInterface {
                })
        }
 
+       static async #getAll (store: string): Promise<SafeRecord[]> {
+               const transaction = this.#storage.transaction(store, 'readonly')
+               const db = transaction.objectStore(store)
+               return new Promise((resolve, reject) => {
+                       const request = db.getAll()
+                       request.onsuccess = (event) => {
+                               resolve((event.target as IDBRequest).result)
+                       }
+                       request.onerror = (event) => {
+                               console.error('Database error')
+                               reject((event.target as IDBRequest).error)
+                       }
+               })
+       }
+
        static #isDataValid (data: unknown): asserts data is { [key: string]: ArrayBuffer } {
                if (typeof data !== 'object') {
                        throw new Error('Invalid data')