From: Chris Duncan Date: Mon, 21 Jul 2025 06:21:45 +0000 (-0700) Subject: Save rolodex data insecurely by using same empty password when storing and fetching... X-Git-Tag: v0.10.5~55^2~50 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=58f4c2de50d08cda0a0342cfd1e3b7d8a662b701;p=libnemo.git Save rolodex data insecurely by using same empty password when storing and fetching since it's not key data. --- diff --git a/src/lib/account.ts b/src/lib/account.ts index 28cae56..a3abc10 100644 --- a/src/lib/account.ts +++ b/src/lib/account.ts @@ -385,8 +385,8 @@ export class Account { } try { - const isLocked = await SafeWorker.assign(privateAccounts) - if (!isLocked) { + const { result } = await SafeWorker.assign(privateAccounts) + if (!result) { throw null } return accounts diff --git a/src/lib/rolodex.ts b/src/lib/rolodex.ts index 2d1a886..73df66f 100644 --- a/src/lib/rolodex.ts +++ b/src/lib/rolodex.ts @@ -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 { + async add (name: string, address: string): Promise { 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({ + 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({ + 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} 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 { + try { + const response = await SafeWorker.assign({ + 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} 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 { + try { + const response = await SafeWorker.assign({ + 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} Promise for a list of all names stored in the rolodex */ - getAllNames (): string[] { - return this.#entries.map(e => e.name) + async getAllNames (): Promise { + try { + const response = await SafeWorker.assign({ + method: 'export', + name: '', + store: 'Rolodex', + password: '' + }) + return Object.keys(response).filter(v => v.slice(0, 5) !== 'nano_') + } catch (err) { + console.log(err) + return [] + } } /** diff --git a/src/lib/workers/safe.ts b/src/lib/workers/safe.ts index 4df0296..7251688 100644 --- a/src/lib/workers/safe.ts +++ b/src/lib/workers/safe.ts @@ -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> { + static async fetch (name: string | string[] | unknown, store: string | unknown, password: ArrayBuffer | unknown, all: boolean = false): Promise> { 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 = {} 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 { + static async #get (names: string[], store: string): Promise { 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 { + 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')