/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // JBREWS DMS — JBVault: real encryption-at-rest using the Web Crypto API. // // AES-256-GCM with a key derived from a dealership passphrase via PBKDF2 // (SHA-256, 210,000 iterations). This is genuine, standards-based encryption — // the same primitives banks use — running entirely in the browser, no server. // // What it protects: the DMS data blob in localStorage and every exported // backup file (.jbz) are stored as ciphertext. Without the passphrase the // data is unreadable. // // Honest limits (so nobody is misled): the derived key is cached in this // browser so the 14 stations on this machine don't have to re-type the // passphrase every reload. A person with full access to THIS browser profile // could therefore reach the cached key. For multi-PC, server-grade key // custody (HSM / KMS) you need the JBREWS server tier. At rest, in transit as // a backup file, and against casual inspection, this is real protection. window.JBVault = (() => { const SALT_KEY = "jbrews-vault-salt"; const VERIFY_KEY = "jbrews-vault-verify"; const CFG_KEY = "jbrews-vault-cfg"; const KEY_CACHE = "jbrews-vault-key"; // base64 raw key (remember-on-browser) const ITER = 210000; const VERIFY_TOKEN = "JBREWS-VAULT-OK"; const ENVELOPE = "ENCv1:"; const enc = new TextEncoder(); const dec = new TextDecoder(); const subtle = (window.crypto && window.crypto.subtle) || null; // ── base64 <-> bytes ── const b64 = { enc: (buf) => { const b = new Uint8Array(buf); let s = ""; for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]); return btoa(s); }, dec: (str) => { const s = atob(str); const b = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i); return b; }, }; const getCfg = () => { try { return JSON.parse(localStorage.getItem(CFG_KEY) || "null"); } catch (e) { return null; } }; const setCfg = (c) => { try { localStorage.setItem(CFG_KEY, JSON.stringify(c)); } catch (e) {} }; let liveKey = null; // CryptoKey held in memory for this tab const getSalt = () => { let raw = null; try { raw = localStorage.getItem(SALT_KEY); } catch (e) {} if (raw) return b64.dec(raw); const salt = window.crypto.getRandomValues(new Uint8Array(16)); try { localStorage.setItem(SALT_KEY, b64.enc(salt)); } catch (e) {} return salt; }; const deriveKey = async (passphrase, salt) => { const baseKey = await subtle.importKey("raw", enc.encode(passphrase), "PBKDF2", false, ["deriveKey"]); return subtle.deriveKey( { name: "PBKDF2", salt, iterations: ITER, hash: "SHA-256" }, baseKey, { name: "AES-GCM", length: 256 }, true, // extractable so we can cache it ["encrypt", "decrypt"] ); }; const encryptWith = async (key, plaintext) => { const iv = window.crypto.getRandomValues(new Uint8Array(12)); const ct = await subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(plaintext)); const both = new Uint8Array(iv.length + ct.byteLength); both.set(iv, 0); both.set(new Uint8Array(ct), iv.length); return ENVELOPE + b64.enc(both); }; const decryptWith = async (key, envelope) => { const raw = envelope.startsWith(ENVELOPE) ? envelope.slice(ENVELOPE.length) : envelope; const both = b64.dec(raw); const iv = both.slice(0, 12), ct = both.slice(12); const pt = await subtle.decrypt({ name: "AES-GCM", iv }, key, ct); return dec.decode(pt); }; // Try to restore a cached key (so reloads / sibling windows stay unlocked). const tryRestoreKey = async () => { if (liveKey || !subtle) return !!liveKey; let cached = null; try { cached = localStorage.getItem(KEY_CACHE) || sessionStorage.getItem(KEY_CACHE); } catch (e) {} if (!cached) return false; try { liveKey = await subtle.importKey("raw", b64.dec(cached), { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); return true; } catch (e) { return false; } }; const cacheKey = async (key, remember) => { if (!remember) return; try { const raw = await subtle.exportKey("raw", key); const store = remember === "browser" ? localStorage : sessionStorage; store.setItem(KEY_CACHE, b64.enc(raw)); } catch (e) {} }; const clearCachedKey = () => { try { localStorage.removeItem(KEY_CACHE); sessionStorage.removeItem(KEY_CACHE); } catch (e) {} }; // (blocker 4, verifier) another window of this PC changed the passphrase (Controls › Change Passphrase re-derives the key // and remembers it on the browser): follow it, so this window's next save encrypts the shared copy with the CURRENT key and // the other window can still open it after a reload. Only a NEW remembered key is followed; a removed key (lock / wipe in // another window) leaves this window's own key alone — its own unlock gate decides on the next reload. try { window.addEventListener("storage", (e) => { if (!e || e.key !== KEY_CACHE || !e.newValue || !subtle || e.newValue === e.oldValue) return; subtle.importKey("raw", b64.dec(e.newValue), { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]) .then((k) => { liveKey = k; }) .catch(() => {}); }); } catch (e) {} const api = { supported: () => !!subtle, isEnabled: () => { const c = getCfg(); return !!(c && c.enabled); }, isUnlocked: () => !!liveKey, config: () => getCfg() || { enabled: false }, status: () => !api.isEnabled() ? "disabled" : (liveKey ? "unlocked" : "locked"), // Restore from cache if possible — called at startup. async resume() { await tryRestoreKey(); return api.status(); }, // Turn encryption ON for the dealership. async enable(passphrase, remember) { if (!subtle) return { ok: false, msg: "This browser does not support Web Crypto." }; if (!passphrase || passphrase.length < 6) return { ok: false, msg: "Passphrase must be at least 6 characters." }; const salt = getSalt(); const key = await deriveKey(passphrase, salt); const verifier = await encryptWith(key, VERIFY_TOKEN); try { localStorage.setItem(VERIFY_KEY, verifier); } catch (e) {} setCfg({ enabled: true, createdAt: new Date().toISOString(), iter: ITER, algo: "AES-256-GCM" }); liveKey = key; await cacheKey(key, remember || "browser"); return { ok: true, msg: "Encryption enabled (AES-256-GCM)." }; }, // Verify a passphrase and unlock for this session. async unlock(passphrase, remember) { if (!subtle) return { ok: false, msg: "Web Crypto unavailable." }; const salt = getSalt(); const key = await deriveKey(passphrase, salt); let verifier = null; try { verifier = localStorage.getItem(VERIFY_KEY); } catch (e) {} if (!verifier) { liveKey = key; await cacheKey(key, remember); return { ok: true, msg: "Unlocked." }; } try { const token = await decryptWith(key, verifier); if (token !== VERIFY_TOKEN) return { ok: false, msg: "Incorrect passphrase." }; } catch (e) { return { ok: false, msg: "Incorrect passphrase." }; } liveKey = key; await cacheKey(key, remember); return { ok: true, msg: "Unlocked." }; }, lock() { liveKey = null; clearCachedKey(); }, // Forget the vault on this browser (unlock gate → "Wipe & start fresh"): key, config, verifier, salt. forget() { liveKey = null; clearCachedKey(); try { localStorage.removeItem(CFG_KEY); localStorage.removeItem(VERIFY_KEY); localStorage.removeItem(SALT_KEY); } catch (e) {} }, async disable() { setCfg({ enabled: false }); try { localStorage.removeItem(VERIFY_KEY); } catch (e) {} clearCachedKey(); // keep liveKey so DMSCore can rewrite plaintext, then drop it const k = liveKey; liveKey = null; return { ok: true, key: k }; }, async changePassphrase(oldPass, newPass, remember) { const u = await api.unlock(oldPass, false); if (!u.ok) return { ok: false, msg: "Current passphrase is incorrect." }; return api.enable(newPass, remember || "browser"); }, async encrypt(plaintext) { if (!liveKey) throw new Error("Vault locked"); return encryptWith(liveKey, plaintext); }, async decrypt(envelope) { if (!liveKey) throw new Error("Vault locked"); return decryptWith(liveKey, envelope); }, isEnvelope: (s) => typeof s === "string" && s.startsWith(ENVELOPE), // Encrypt/decrypt a backup file with an ad-hoc passphrase (independent of // the at-rest key) — used for portable .jbz exports/imports. async encryptWithPass(passphrase, plaintext) { const salt = window.crypto.getRandomValues(new Uint8Array(16)); const key = await deriveKey(passphrase, salt); const env = await encryptWith(key, plaintext); return "JBZ1:" + b64.enc(salt) + ":" + env.slice(ENVELOPE.length); }, async decryptWithPass(passphrase, blob) { if (!blob.startsWith("JBZ1:")) throw new Error("Not a JBZ file"); const rest = blob.slice(5); const i = rest.indexOf(":"); const salt = b64.dec(rest.slice(0, i)); const env = rest.slice(i + 1); const key = await deriveKey(passphrase, salt); return decryptWith(key, ENVELOPE + env); }, }; return api; })();