import { Entropy } from '#src/lib/entropy.js'
import { WorkerInterface } from '#src/lib/pool.js'
+type SafeRecord = {
+ encrypted: string,
+ iv: string
+}
+
type SafeInput = {
method: string
name: string
result: any
}
-const ERR_MSG = 'Failed to store item in Safe'
-
/**
* Encrypts and stores data in the browser using IndexedDB.
*/
export class Safe extends WorkerInterface {
- static #storage: Storage = globalThis.sessionStorage
+ static DB_NAME = 'libnemo'
+ static STORE_NAME = 'Safe'
+ static ERR_MSG = 'Failed to store item in Safe'
+ static #storage: IDBDatabase
static {
Safe.listen()
}
static async work (data: any[]): Promise<any[]> {
+ this.#storage = await this.#open(this.DB_NAME)
const results: SafeOutput[] = []
for (const d of data) {
console.log(d)
/**
* Removes data from the Safe without decrypting.
*/
- static destroy (name: string): boolean {
+ static async destroy (name: string): Promise<boolean> {
try {
- this.#storage.removeItem(name)
+ return await this.#delete(name)
} catch (err) {
- throw new Error(ERR_MSG)
+ throw new Error(this.ERR_MSG)
}
- return (this.#storage.getItem(name) == null)
}
/**
- * Encrypts data with a password as bytes and stores it in the Safe.
+ * Encrypts data with a password byte array and stores it in the Safe.
*/
static async put (name: string, password: Uint8Array, data: any): Promise<boolean> {
- if (this.#storage.getItem(name)) {
+ if (await this.#exists(name)) {
password.fill(0)
- throw new Error(ERR_MSG)
+ throw new Error(this.ERR_MSG)
}
return this.overwrite(name, password, data)
}
/**
- * Encrypts data with a password as bytes and stores it in the Safe.
+ * Encrypts data with a password byte array and stores it in the Safe.
*/
static async overwrite (name: string, password: Uint8Array, data: any): Promise<boolean> {
let passkey: CryptoKey
try {
passkey = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
} catch {
- throw new Error(ERR_MSG)
+ throw new Error(this.ERR_MSG)
} finally {
password.fill(0)
}
if (this.#isInvalid(name, passkey, data)) {
- throw new Error(ERR_MSG)
+ throw new Error(this.ERR_MSG)
}
try {
encrypted: buffer.toHex(encrypted),
iv: iv.hex
}
- this.#storage.setItem(name, JSON.stringify(record))
+ return await this.#put(record, name)
} catch (err) {
- throw new Error(ERR_MSG)
+ throw new Error(this.ERR_MSG)
}
-
- return (this.#storage.getItem(name) != null)
}
/**
- * Retrieves data from the Safe and decrypts data with a password as bytes.
+ * Retrieves data from the Safe and decrypts it with a password byte array.
*/
static async get (name: string, password: Uint8Array): Promise<any> {
let passkey: CryptoKey
return null
}
- let item
+ let record: SafeRecord
try {
- item = this.#storage.getItem(name)
+ record = await this.#get(name)
} catch {
return null
}
- if (item == null) {
+ if (record == null) {
return null
}
- const record = JSON.parse(item)
const encrypted = hex.toBytes(record.encrypted)
const iv = await Entropy.import(record.iv)
}
return false
}
+
+ static async #exists (name: string): Promise<boolean> {
+ return await this.#get(name) !== undefined
+ }
+
+ static async #delete (name: string): Promise<boolean> {
+ try {
+ return await this.#transact<undefined>('readwrite', db => db.delete(name)) === undefined
+ } catch {
+ throw new Error(this.ERR_MSG)
+ }
+ }
+
+ static async #get (name: string): Promise<SafeRecord> {
+ try {
+ return await this.#transact<SafeRecord>('readonly', db => db.get(name))
+ } catch {
+ throw new Error(this.ERR_MSG)
+ }
+ }
+
+ 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
+ if (!db.objectStoreNames.contains(this.STORE_NAME)) {
+ db.createObjectStore(this.STORE_NAME)
+ }
+ }
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBOpenDBRequest).result)
+ }
+ request.onerror = (event) => {
+ reject(new Error('Failed to open IndexedDB', { cause: event }))
+ }
+ })
+ }
+
+ static async #put (record: SafeRecord, name: string): Promise<boolean> {
+ const result = await this.#transact<typeof name>('readwrite', db => db.put(record, name))
+ return await this.#exists(result)
+ }
+
+ static async #transact<T> (mode: IDBTransactionMode, method: (db: IDBObjectStore) => IDBRequest): Promise<T> {
+ const db = this.#storage.transaction(this.STORE_NAME, mode).objectStore(this.STORE_NAME)
+ return new Promise((resolve, reject) => {
+ const request = method(db)
+ request.onsuccess = (event) => {
+ resolve((event.target as IDBRequest).result)
+ }
+ request.onerror = (event) => {
+ console.error('IndexedDB transaction error:', (event.target as IDBRequest).error)
+ reject((event.target as IDBRequest).error)
+ }
+ })
+ }
}
export default `