--- /dev/null
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+'use strict'
+
+import { NamedData } from '#types'
+
+/**
+* Encrypts and stores data in the browser using IndexedDB.
+*/
+export class Safe {
+ static DB_NAME = 'libnemo'
+ static DB_STORES = ['Wallet', 'Account', 'Rolodex']
+ static #storage: IDBDatabase
+
+ /**
+ * Deletes records from a datastore.
+ *
+ * @param {string[]} names - Index keys of the records to delete
+ * @param {string} store - Datastore from which to delete records
+ * @returns {Promise<boolean>} True if data was successfully removed, else false
+ */
+ static async delete (names: string[], store: string): Promise<boolean> {
+ this.#storage ??= await this.#open(this.DB_NAME)
+ const transaction = this.#storage.transaction(store, 'readwrite')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const requests = names.map(name => db.delete(name))
+ transaction.oncomplete = (event) => {
+ for (const request of requests) {
+ if (request?.error != null) {
+ reject(request.error)
+ }
+ if (request.result !== undefined) {
+ resolve(false)
+ }
+ }
+ resolve(true)
+ }
+ transaction.onerror = (event) => {
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
+
+ /**
+ * Gets specific records from a datastore.
+ *
+ * @param {string[]} names - Index keys of the records to get
+ * @param {string} store - Datastore from which to get records
+ * @returns {Promise<NamedData>} Object of key-value pairs
+ */
+ static async get (names: string[], store: string): Promise<NamedData> {
+ this.#storage ??= await this.#open(this.DB_NAME)
+ const transaction = this.#storage.transaction(store, 'readonly')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const requests = names.map(name => db.get(name))
+ transaction.oncomplete = (event) => {
+ const results: NamedData = {}
+ for (const request of requests) {
+ results[request.result.id] = request.error ?? request.result
+ }
+ resolve(results)
+ }
+ transaction.onerror = (event) => {
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
+
+ /**
+ * Gets all records from a specific datastore.
+ *
+ * @param {string} store - Datastore from which to get records
+ * @returns {Promise<NamedData>} Object of key-value pairs
+ */
+ static async getAll (store: string): Promise<NamedData> {
+ this.#storage ??= await this.#open(this.DB_NAME)
+ const transaction = this.#storage.transaction(store, 'readonly')
+ const db = transaction.objectStore(store)
+ return new Promise((resolve, reject) => {
+ const request = db.getAll()
+ transaction.oncomplete = (event) => {
+ if (request.error != null) {
+ reject(request.error)
+ } else if (request.result == null) {
+ reject('getAll request failed')
+ } else {
+ const results: NamedData = {}
+ for (const result of request.result) {
+ results[result.id] = request.error ?? request.result
+ }
+ resolve(results)
+ }
+ }
+ transaction.onerror = (event) => {
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
+
+ /**
+ * Inserts records in a datastore, overwriting existing data.
+ *
+ * @param {NamedData} data - Object of key-value pairs
+ * @param {string} store - Datastore in which to put records
+ * @returns {Promise<(IDBValidKey | DOMException)[]>} Index keys of the records inserted
+ */
+ static async put (data: NamedData, store: string): Promise<(IDBValidKey | DOMException)[]> {
+ this.#storage ??= await this.#open(this.DB_NAME)
+ 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] }))
+ transaction.oncomplete = (event) => {
+ const results = []
+ for (const request of requests) {
+ results.push(request.error ?? request.result)
+ }
+ resolve(results)
+ }
+ transaction.onerror = (event) => {
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
+
+ static async #open (database: string): Promise<IDBDatabase> {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(database, 1)
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result
+ for (const DB_STORE of this.DB_STORES) {
+ if (!db.objectStoreNames.contains(DB_STORE)) {
+ db.createObjectStore(DB_STORE, { keyPath: 'id' })
+ }
+ }
+ }
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBOpenDBRequest).result)
+ }
+ request.onerror = (event) => {
+ reject(new Error('Database error', { cause: event }))
+ }
+ })
+ }
+}
try {
const { purpose, password, salt } = this.#extractData(data)
const key = await this.#createAesKey(purpose, password, salt)
+ new Uint8Array(password).fill(0).buffer.transfer()
//@ts-expect-error
BROWSER: postMessage({ key, salt }, [key, salt])
//@ts-expect-error
throw new TypeError('Invalid key purpose')
}
const purpose: 'encrypt' | 'decrypt' = dataObject.purpose
- if (purpose === 'decrypt' && !('salt' in dataObject)) {
+ if (purpose === 'decrypt' && !(dataObject.salt instanceof ArrayBuffer)) {
throw new TypeError('Salt required for decryption key')
}
- if (dataObject.salt != null && !(dataObject.salt instanceof ArrayBuffer)) {
- throw new TypeError('Salt must be ArrayBuffer')
- }
- const salt: ArrayBuffer = dataObject.salt ?? globalThis.crypto.getRandomValues(new Uint8Array(32)).buffer
+ const salt: ArrayBuffer = purpose === 'decrypt' && dataObject.salt instanceof ArrayBuffer
+ ? dataObject.salt
+ : crypto.getRandomValues(new Uint8Array(32)).buffer
return { purpose, password, salt }
}
static async #createAesKey (purpose: 'encrypt' | 'decrypt', password: ArrayBuffer, salt: ArrayBuffer): Promise<CryptoKey> {
- const derivationKey = await globalThis.crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+ const derivationKey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
const derivationAlgorithm: Pbkdf2Params = {
name: 'PBKDF2',
hash: 'SHA-512',
name: 'AES-GCM',
length: 256
}
- return await globalThis.crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
+ return await crypto.subtle.deriveKey(derivationAlgorithm, derivationKey, derivedKeyType, false, [purpose])
}
}