// IndexedDB persistence: the Signal protocol store + app data (contacts, chats). // Private keys live ONLY here, in the browser. They are never sent to the server. const DB_NAME = "cipher"; const DB_VERSION = 1; function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; // Signal protocol material (identity keypair, prekeys, sessions, peer identities). if (!db.objectStoreNames.contains("signal")) db.createObjectStore("signal"); // App metadata (my account, settings). if (!db.objectStoreNames.contains("meta")) db.createObjectStore("meta"); // Known contacts, keyed by userId. if (!db.objectStoreNames.contains("contacts")) db.createObjectStore("contacts", { keyPath: "userId" }); // Message history, keyed by autoincrement, indexed by conversation. if (!db.objectStoreNames.contains("messages")) { const s = db.createObjectStore("messages", { keyPath: "id", autoIncrement: true }); s.createIndex("convo", "convo"); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } let _db; async function db() { if (!_db) _db = await openDB(); return _db; } function tx(store, mode = "readonly") { return db().then((d) => d.transaction(store, mode).objectStore(store)); } function reqP(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // ---- generic KV helpers over the "signal" and "meta" stores ---- async function kvGet(store, key) { return reqP((await tx(store)).get(key)); } async function kvPut(store, key, val) { return reqP((await tx(store, "readwrite")).put(val, key)); } async function kvDel(store, key) { return reqP((await tx(store, "readwrite")).delete(key)); } export const meta = { get: (k) => kvGet("meta", k), set: (k, v) => kvPut("meta", k, v), del: (k) => kvDel("meta", k), }; // ---- contacts ---- export const contacts = { async put(contact) { return reqP((await tx("contacts", "readwrite")).put(contact)); }, async get(userId) { return reqP((await tx("contacts")).get(userId)); }, async all() { return reqP((await tx("contacts")).getAll()); }, }; // ---- messages ---- export const messages = { async add(msg) { return reqP((await tx("messages", "readwrite")).add(msg)); }, async forConvo(convo) { const idx = (await tx("messages")).index("convo"); return reqP(idx.getAll(convo)); }, }; // ---- Signal protocol store ---- // Implements the StorageType interface libsignal expects. Keys are strings; // ArrayBuffers and records are stored directly (IndexedDB structured-clones them). export class SignalStore { Direction = { SENDING: 1, RECEIVING: 2 }; async getIdentityKeyPair() { return kvGet("signal", "identityKey"); } async getLocalRegistrationId() { return kvGet("signal", "registrationId"); } async setIdentity(keyPair, registrationId) { await kvPut("signal", "identityKey", keyPair); await kvPut("signal", "registrationId", registrationId); } // Trust-on-first-use: trust a new peer identity; reject if it silently changes. async isTrustedIdentity(identifier, identityKey /*, direction */) { const existing = await kvGet("signal", "identity/" + identifier); if (!existing) return true; return buffersEqual(existing, identityKey); } async saveIdentity(identifier, identityKey) { const addr = identifier.split(".")[0]; const existing = await kvGet("signal", "identity/" + addr); await kvPut("signal", "identity/" + addr, identityKey); return !!existing && !buffersEqual(existing, identityKey); // true = identity changed } async loadIdentityKey(identifier) { return kvGet("signal", "identity/" + identifier.split(".")[0]); } async loadPreKey(keyId) { return kvGet("signal", "prekey/" + keyId); } async storePreKey(keyId, keyPair) { return kvPut("signal", "prekey/" + keyId, keyPair); } async removePreKey(keyId) { return kvDel("signal", "prekey/" + keyId); } async loadSignedPreKey(keyId) { return kvGet("signal", "signedprekey/" + keyId); } async storeSignedPreKey(keyId, keyPair) { return kvPut("signal", "signedprekey/" + keyId, keyPair); } async removeSignedPreKey(keyId) { return kvDel("signal", "signedprekey/" + keyId); } async loadSession(identifier) { return kvGet("signal", "session/" + identifier); } async storeSession(identifier, record) { return kvPut("signal", "session/" + identifier, record); } async removeSession(identifier) { return kvDel("signal", "session/" + identifier); } async removeAllSessions() { /* not needed for MVP */ } } function buffersEqual(a, b) { const ua = new Uint8Array(a); const ub = new Uint8Array(b); if (ua.length !== ub.length) return false; let diff = 0; for (let i = 0; i < ua.length; i++) diff |= ua[i] ^ ub[i]; return diff === 0; }