Encrypted Offline Notepad as Local HTML FilesteemCreated with Sketch.

in #coding2 days ago (edited)

This script is a local protected “notepad” implemented as a single HTML-file. The file is stored directly on the computer and opened in browser without an internet connection. The script does not use servers, cloud services, or external APIs, and all data remains only on this device and not sent anywhere. All data stored in the browser’s local IndexedDB database . This DB exists only within the specific browser in which the script is run. The note text and service data are stored inside the browser.

On the first run the user sets a password. Without the correct password access to main functions is not possible. The password is not stored in plain form and used only for verification and for creating the encryption key. Only a salted password hash is stored in the database , and the password itself exists only in the browser’s memory while the file is open. The note text is always stored in encrypted form. Before saving, it is encrypted using AES-GCM. The encryption key is derived from the password and is additionally hardened by multiple hash computations to make password bruteforcing more difficult. If password entered incorrectly, the data cannot be decrypted. The password can be changed after authorization. In this case the text is first decrypted with the old password and immediately reencrypted with new passw. And the old password no longer works after that.

The script provides the ability to create a backup of data as JSON file and to restore data from such a backup. Even if backup is available, it’s impossible to access the text without the correct password.

A full data deletion function is also provided. After user confirmation, the local database is completely deleted along with the password and the encrypted text. This allows all data to be fully cleared in case of urgent need.

The script is intended for local use and for protecting personal notes from accidental access. It’s not a replacement for system-level disk encryption and cannot recover data if password was lost.

Screenshots:

2026-01-31_185018.png

2026-01-31_185038.png

2026-01-31_185108.png

2026-01-31_185301.png

Download file (save as!!!) notes.html.jpg, and rename to notes.html

Full code:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Secure Notes</title>
<style>
body {
  font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  margin: 24px;
  color: #111;
}
.wrap {
  max-width: 900px;
  margin: 0 auto;
}
h1 {
  font-size: 20px;
  margin: 0 0 14px;
}
.card {
  border: 1px solid #ddd;
  border-radius: 12px;
  padding: 16px;
  margin: 12px 0;
  background: #fff;
}
.row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: center;
}
label {
  display: block;
  font-size: 13px;
  color: #444;
  margin-bottom: 6px;
}
input[type="password"],
input[type="text"] {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 10px;
  width: min(360px, 100%);
}
textarea {
  width: 95%;
  min-height: 320px;
  padding: 12px;
  border: 1px solid #ccc;
  border-radius: 12px;
  resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
button {
  padding: 10px 14px;
  border: 1px solid #ccc;
  border-radius: 10px;
  background: #f6f6f6;
  cursor: pointer;
}
button:hover {
  background: #efefef;
}
.muted {
  color: #666;
  font-size: 13px;
  line-height: 1.35;
}
.ok {
  color: #0a7a2f;
}
.err {
  color: #b00020;
}
.hidden {
  display: none;
}
.spacer {
  height: 8px;
}
.hr {
  height: 1px;
  background: #eee;
  margin: 12px 0;
}
</style>
</head>
<body>
<div class="wrap">
  <h1>Secure Notes</h1>

  <div id="status" class="muted">&nbsp;</div>

  <div id="authCard" class="card">
    <div id="setupBlock" class="hidden">
      <div class="row">
        <div style="flex:1; min-width:260px;">
          <label>Create a password (first run)</label>
          <input id="setupPass1" type="password" autocomplete="new-password" placeholder="New password" />
          <div class="spacer"></div>
          <input id="setupPass2" type="password" autocomplete="new-password" placeholder="Repeat password" />
        </div>
      </div>
      <div class="spacer"></div>
      <button id="btnSetup">Set password</button>
      <div class="spacer"></div>
      <div class="muted">
        The password is stored as <b>MD5(salt + password)</b> in IndexedDB. The note is stored encrypted (AES-GCM).
      </div>
    </div>

    <div id="loginBlock" class="hidden">
      <div class="row">
        <div style="flex:1; min-width:260px;">
          <label>Enter password</label>
          <input id="loginPass" type="password" autocomplete="current-password" placeholder="Password" />
        </div>
        <div style="display:flex; gap:10px; align-items:flex-end;">
          <button id="btnLogin">Unlock</button>
          <button id="btnLogout" class="hidden">Lock</button>
        </div>
      </div>
      <div class="spacer"></div>
      <div class="muted">
        If you forget the password, the note cannot be recovered (it is actually encrypted).
      </div>
    </div>
  </div>

  <div id="editorCard" class="card hidden">
    <div class="row" style="justify-content:space-between;">
      <div class="muted">
        Status: <span id="authState" class="ok">Unlocked</span>
      </div>
      <div class="row">
        <button id="btnSave">Save</button>
        <button id="btnBackup">Backup</button>
        <label style="margin:0;">
          <input id="fileRestore" type="file" accept=".json,application/json" style="display:none;" />
          <button id="btnRestore" type="button">Restore</button>
        </label>
        <button id="btnWipe">Wipe DB</button>
      </div>
    </div>

    <div class="hr"></div>

    <label>Note (stored encrypted)</label>
    <textarea id="note"></textarea>
    <div class="spacer"></div>
    <div class="muted" id="saveHint"></div>

    <div class="hr"></div>

    <div>
      <div class="row">
        <div style="flex:1; min-width:260px;">
          <label>Change password</label>
          <input id="oldPass" type="password" autocomplete="current-password" placeholder="Current password" />
          <div class="spacer"></div>
          <input id="newPass1" type="password" autocomplete="new-password" placeholder="New password" />
          <div class="spacer"></div>
          <input id="newPass2" type="password" autocomplete="new-password" placeholder="Repeat new password" />
        </div>
      </div>
      <div class="spacer"></div>
      <button id="btnChangePass">Update password</button>
      <div class="spacer"></div>
      <div class="muted">
        When you change the password, the note is re-encrypted with the new key.
      </div>
    </div>
  </div>
</div>

<script>
/**
 * Secure Notepad (single local HTML file)
 * - IndexedDB storage
 * - Password stored as MD5(saltB64 + password) (as requested)
 * - Note encrypted using AES-GCM
 * - Key derived from password via repeated SHA-256 rounds (simple offline stretching)
 * - Backup/Restore to/from JSON file
 * - nowISO() uses fixed GMT+2 (+02:00)
 * - Status messages auto-hide after 4s (except init prompts)
 * - Enter key triggers actions (login/setup/change password)
 * - Wipe DB button with strong confirmation (type DELETE)
 */

const DB_NAME = "secure_notepad_db_v1";
const STORE = "kv";
const KEY_AUTH = "auth";
const KEY_NOTE = "note";

let db = null;
let session = {
  unlocked: false,
  password: null // kept only in memory while this page is open
};

const el = (id) => document.getElementById(id);

// Fixed GMT+2 ISO timestamp (always +02:00)
function nowISO() {
  const offsetMinutes = 120; // GMT+2
  const d = new Date(Date.now() + offsetMinutes * 60 * 1000);

  const pad = (n, w = 2) => String(n).padStart(w, "0");

  const yyyy = d.getUTCFullYear();
  const mm   = pad(d.getUTCMonth() + 1);
  const dd   = pad(d.getUTCDate());
  const hh   = pad(d.getUTCHours());
  const mi   = pad(d.getUTCMinutes());
  const ss   = pad(d.getUTCSeconds());
  const ms   = pad(d.getUTCMilliseconds(), 3);

  return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}.${ms}+02:00`;
}

// Status with auto-hide
let statusTimer = null;

function setStatus(msg, cls = "muted", autoHideMs = 4000) {
  const s = el("status");
  s.className = cls;

  // If msg is empty/null -> keep layout stable with NBSP
  if (!msg) {
    s.innerHTML = "&nbsp;";
    if (statusTimer) clearTimeout(statusTimer);
    return;
  }

  // Use textContent for normal messages (safe), but keep NBSP for "hidden"
  s.textContent = msg;

  if (statusTimer) clearTimeout(statusTimer);

  if (autoHideMs > 0) {
    statusTimer = setTimeout(() => {
      s.innerHTML = "&nbsp;"; // keep height, no page jump
    }, autoHideMs);
  }
}

function bytesToB64(bytes) {
  let binary = "";
  const arr = new Uint8Array(bytes);
  for (let i = 0; i < arr.length; i++) binary += String.fromCharCode(arr[i]);
  return btoa(binary);
}

function b64ToBytes(b64) {
  const binary = atob(b64);
  const out = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
  return out;
}

function concatBytes(a, b) {
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0);
  out.set(b, a.length);
  return out;
}

function textToBytes(str) {
  return new TextEncoder().encode(str);
}

function bytesToText(bytes) {
  return new TextDecoder().decode(bytes);
}

function randomBytes(n) {
  const a = new Uint8Array(n);
  crypto.getRandomValues(a);
  return a;
}

async function sha256(bytes) {
  const buf = await crypto.subtle.digest("SHA-256", bytes);
  return new Uint8Array(buf);
}

/**
 * Simple key derivation: repeated SHA-256 over (salt + password bytes)
 * This is not PBKDF2 (kept simple/offline), but still slows brute-force a bit.
 */
async function deriveKeyBytes(password, saltBytes, rounds = 120000) {
  let data = concatBytes(saltBytes, textToBytes(password));
  let h = await sha256(data);
  for (let i = 0; i < rounds; i++) {
    h = await sha256(h);
  }
  return h; // 32 bytes
}

async function importAesKey(keyBytes32) {
  return crypto.subtle.importKey(
    "raw",
    keyBytes32,
    { name: "AES-GCM" },
    false,
    ["encrypt", "decrypt"]
  );
}

/* MD5 implementation (pure JS) */
function md5(str) {
  function cmn(q, a, b, x, s, t) {
    a = add32(add32(a, q), add32(x, t));
    return add32((a << s) | (a >>> (32 - s)), b);
  }
  function ff(a, b, c, d, x, s, t) { return cmn((b & c) | (~b & d), a, b, x, s, t); }
  function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & ~d), a, b, x, s, t); }
  function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); }
  function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | ~d), a, b, x, s, t); }

  function md5cycle(x, k) {
    let a = x[0], b = x[1], c = x[2], d = x[3];

    a = ff(a, b, c, d, k[0], 7, -680876936);
    d = ff(d, a, b, c, k[1], 12, -389564586);
    c = ff(c, d, a, b, k[2], 17,  606105819);
    b = ff(b, c, d, a, k[3], 22, -1044525330);
    a = ff(a, b, c, d, k[4], 7, -176418897);
    d = ff(d, a, b, c, k[5], 12,  1200080426);
    c = ff(c, d, a, b, k[6], 17, -1473231341);
    b = ff(b, c, d, a, k[7], 22, -45705983);
    a = ff(a, b, c, d, k[8], 7,  1770035416);
    d = ff(d, a, b, c, k[9], 12, -1958414417);
    c = ff(c, d, a, b, k[10], 17, -42063);
    b = ff(b, c, d, a, k[11], 22, -1990404162);
    a = ff(a, b, c, d, k[12], 7,  1804603682);
    d = ff(d, a, b, c, k[13], 12, -40341101);
    c = ff(c, d, a, b, k[14], 17, -1502002290);
    b = ff(b, c, d, a, k[15], 22,  1236535329);

    a = gg(a, b, c, d, k[1], 5, -165796510);
    d = gg(d, a, b, c, k[6], 9, -1069501632);
    c = gg(c, d, a, b, k[11], 14,  643717713);
    b = gg(b, c, d, a, k[0], 20, -373897302);
    a = gg(a, b, c, d, k[5], 5, -701558691);
    d = gg(d, a, b, c, k[10], 9,  38016083);
    c = gg(c, d, a, b, k[15], 14, -660478335);
    b = gg(b, c, d, a, k[4], 20, -405537848);
    a = gg(a, b, c, d, k[9], 5,  568446438);
    d = gg(d, a, b, c, k[14], 9, -1019803690);
    c = gg(c, d, a, b, k[3], 14, -187363961);
    b = gg(b, c, d, a, k[8], 20,  1163531501);
    a = gg(a, b, c, d, k[13], 5, -1444681467);
    d = gg(d, a, b, c, k[2], 9, -51403784);
    c = gg(c, d, a, b, k[7], 14,  1735328473);
    b = gg(b, c, d, a, k[12], 20, -1926607734);

    a = hh(a, b, c, d, k[5], 4, -378558);
    d = hh(d, a, b, c, k[8], 11, -2022574463);
    c = hh(c, d, a, b, k[11], 16,  1839030562);
    b = hh(b, c, d, a, k[14], 23, -35309556);
    a = hh(a, b, c, d, k[1], 4, -1530992060);
    d = hh(d, a, b, c, k[4], 11,  1272893353);
    c = hh(c, d, a, b, k[7], 16, -155497632);
    b = hh(b, c, d, a, k[10], 23, -1094730640);
    a = hh(a, b, c, d, k[13], 4,  681279174);
    d = hh(d, a, b, c, k[0], 11, -358537222);
    c = hh(c, d, a, b, k[3], 16, -722521979);
    b = hh(b, c, d, a, k[6], 23,  76029189);
    a = hh(a, b, c, d, k[9], 4, -640364487);
    d = hh(d, a, b, c, k[12], 11, -421815835);
    c = hh(c, d, a, b, k[15], 16,  530742520);
    b = hh(b, c, d, a, k[2], 23, -995338651);

    a = ii(a, b, c, d, k[0], 6, -198630844);
    d = ii(d, a, b, c, k[7], 10,  1126891415);
    c = ii(c, d, a, b, k[14], 15, -1416354905);
    b = ii(b, c, d, a, k[5], 21, -57434055);
    a = ii(a, b, c, d, k[12], 6,  1700485571);
    d = ii(d, a, b, c, k[3], 10, -1894986606);
    c = ii(c, d, a, b, k[10], 15, -1051523);
    b = ii(b, c, d, a, k[1], 21, -2054922799);
    a = ii(a, b, c, d, k[8], 6,  1873313359);
    d = ii(d, a, b, c, k[15], 10, -30611744);
    c = ii(c, d, a, b, k[6], 15, -1560198380);
    b = ii(b, c, d, a, k[13], 21,  1309151649);
    a = ii(a, b, c, d, k[4], 6, -145523070);
    d = ii(d, a, b, c, k[11], 10, -1120210379);
    c = ii(c, d, a, b, k[2], 15,  718787259);
    b = ii(b, c, d, a, k[9], 21, -343485551);

    x[0] = add32(a, x[0]);
    x[1] = add32(b, x[1]);
    x[2] = add32(c, x[2]);
    x[3] = add32(d, x[3]);
  }

  function md5blk(s) {
    const md5blks = [];
    for (let i = 0; i < 64; i += 4) {
      md5blks[i >> 2] = s.charCodeAt(i) +
        (s.charCodeAt(i + 1) << 8) +
        (s.charCodeAt(i + 2) << 16) +
        (s.charCodeAt(i + 3) << 24);
    }
    return md5blks;
  }

  function md51(s) {
    let n = s.length;
    const state = [1732584193, -271733879, -1732584194, 271733878];
    let i;
    for (i = 64; i <= n; i += 64) md5cycle(state, md5blk(s.substring(i - 64, i)));
    s = s.substring(i - 64);

    const tail = new Array(16).fill(0);
    for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3);
    tail[i >> 2] |= 0x80 << ((i % 4) << 3);
    if (i > 55) { md5cycle(state, tail); for (let j = 0; j < 16; j++) tail[j] = 0; }
    tail[14] = n * 8;
    md5cycle(state, tail);
    return state;
  }

  function rhex(n) {
    let s = "";
    for (let j = 0; j < 4; j++) s += ("0" + ((n >> (j * 8)) & 0xFF).toString(16)).slice(-2);
    return s;
  }

  function hex(x) { for (let i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(""); }
  function add32(a, b) { return (a + b) & 0xFFFFFFFF; }

  return hex(md51(str));
}

/* IndexedDB helpers */
function openDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, 1);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

function idbGet(key) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readonly");
    const st = tx.objectStore(STORE);
    const r = st.get(key);
    r.onsuccess = () => resolve(r.result);
    r.onerror = () => reject(r.error);
  });
}

function idbSet(key, value) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readwrite");
    const st = tx.objectStore(STORE);
    const r = st.put(value, key);
    r.onsuccess = () => resolve(true);
    r.onerror = () => reject(r.error);
  });
}

function idbAll() {
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE, "readonly");
    const st = tx.objectStore(STORE);
    const out = {};
    const r = st.openCursor();
    r.onsuccess = () => {
      const cur = r.result;
      if (cur) { out[cur.key] = cur.value; cur.continue(); }
      else resolve(out);
    };
    r.onerror = () => reject(r.error);
  });
}

/* Crypto: encrypt/decrypt note */
async function encryptNote(plainText, password, saltB64) {
  const salt = b64ToBytes(saltB64);
  const keyBytes = await deriveKeyBytes(password, salt);
  const key = await importAesKey(keyBytes);

  const iv = randomBytes(12); // recommended for AES-GCM
  const pt = textToBytes(plainText);

  const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt);
  return {
    iv: bytesToB64(iv),
    ct: bytesToB64(new Uint8Array(ctBuf))
  };
}

async function decryptNote(noteObj, password, saltB64) {
  if (!noteObj || !noteObj.ct || !noteObj.iv) return "";
  const salt = b64ToBytes(saltB64);
  const keyBytes = await deriveKeyBytes(password, salt);
  const key = await importAesKey(keyBytes);

  const iv = b64ToBytes(noteObj.iv);
  const ct = b64ToBytes(noteObj.ct);

  const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
  return bytesToText(new Uint8Array(ptBuf));
}

/* Auth */
async function authExists() {
  const a = await idbGet(KEY_AUTH);
  return !!a;
}

async function setPassword(newPassword) {
  const salt = randomBytes(16);
  const saltB64 = bytesToB64(salt);
  const passHash = md5(saltB64 + newPassword); // MD5(saltB64 + password)

  await idbSet(KEY_AUTH, {
    saltB64,
    passHash,
    algo: "md5(saltB64+password)",
    createdAt: nowISO()
  });

  // Initialize an empty encrypted note
  const enc = await encryptNote("", newPassword, saltB64);
  await idbSet(KEY_NOTE, { ...enc, updatedAt: nowISO() });
}

async function verifyPassword(password) {
  const a = await idbGet(KEY_AUTH);
  if (!a) return false;
  const check = md5(a.saltB64 + password);
  return check === a.passHash;
}

function showSetup() {
  el("setupBlock").classList.remove("hidden");
  el("loginBlock").classList.add("hidden");
}

function showLogin() {
  el("loginBlock").classList.remove("hidden");
  el("setupBlock").classList.add("hidden");
}

function showEditor(on) {
  el("editorCard").classList.toggle("hidden", !on);
  el("btnLogout").classList.toggle("hidden", !on);
  el("authState").textContent = on ? "Unlocked" : "Locked";
  el("authState").className = on ? "ok" : "err";
}

function showAuthCard(on) {
  el("authCard").classList.toggle("hidden", !on);
}

async function unlock(password) {
  const ok = await verifyPassword(password);
  if (!ok) return false;

  const auth = await idbGet(KEY_AUTH);
  const noteObj = await idbGet(KEY_NOTE);

  try {
    const plain = await decryptNote(noteObj, password, auth.saltB64);
    session.unlocked = true;
    session.password = password;
    el("note").value = plain;
    el("saveHint").textContent = "";
    showEditor(true);
    showAuthCard(false);
    setStatus("Unlocked.", "ok"); // auto-hide 4s
    return true;
  } catch (e) {
    setStatus("Failed to decrypt the data. Wrong password or corrupted storage.", "err");
    return false;
  }
}

function lock() {
  session.unlocked = false;
  session.password = null;
  el("note").value = "";
  showEditor(false);
  showAuthCard(true);
  showLogin();
  setStatus("Locked.", "muted"); // auto-hide 4s
}

async function saveNote() {
  if (!session.unlocked) return;
  const auth = await idbGet(KEY_AUTH);
  const text = el("note").value;
  const enc = await encryptNote(text, session.password, auth.saltB64);
  await idbSet(KEY_NOTE, { ...enc, updatedAt: nowISO() });
  el("saveHint").textContent = "Saved: " + new Date().toLocaleString();
}

async function changePasswordFlow(oldPass, newPass1, newPass2) {
  if (!session.unlocked) { setStatus("Please unlock first.", "err"); return; }
  if (!oldPass || !newPass1 || !newPass2) { setStatus("Fill in all password fields.", "err"); return; }
  if (newPass1 !== newPass2) { setStatus("New passwords do not match.", "err"); return; }
  if (newPass1.length < 4) { setStatus("Password is too short (min 4 characters).", "err"); return; }

  const okOld = await verifyPassword(oldPass);
  if (!okOld) { setStatus("Current password is incorrect.", "err"); return; }

  // Decrypt current note using old password
  const auth = await idbGet(KEY_AUTH);
  const noteObj = await idbGet(KEY_NOTE);
  let plain;
  try {
    plain = await decryptNote(noteObj, oldPass, auth.saltB64);
  } catch (e) {
    setStatus("Could not decrypt current data for password change.", "err");
    return;
  }

  // Create new salt + store MD5 hash
  const newSalt = randomBytes(16);
  const newSaltB64 = bytesToB64(newSalt);
  const newHash = md5(newSaltB64 + newPass1);

  await idbSet(KEY_AUTH, {
    saltB64: newSaltB64,
    passHash: newHash,
    algo: "md5(saltB64+password)",
    updatedAt: nowISO()
  });

  // Re-encrypt note with new key
  const enc = await encryptNote(plain, newPass1, newSaltB64);
  await idbSet(KEY_NOTE, { ...enc, updatedAt: nowISO() });

  // Update session
  session.password = newPass1;

  // Clear UI fields
  el("oldPass").value = "";
  el("newPass1").value = "";
  el("newPass2").value = "";

  setStatus("Password updated. Data re-encrypted.", "ok");
}

function downloadTextFile(filename, text) {
  const blob = new Blob([text], { type: "application/json;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

async function backup() {
  const all = await idbAll();
  const payload = {
    app: "SecureNotepadIndexedDB",
    version: 1,
    createdAt: nowISO(),
    data: all
  };
  downloadTextFile("secure_notepad_backup.json", JSON.stringify(payload, null, 2));
  setStatus("Backup saved to file.", "ok");
}

async function restoreFromFile(file) {
  try {
    const text = await file.text();
    const payload = JSON.parse(text);
    if (!payload || !payload.data || typeof payload.data !== "object") {
      setStatus("Invalid backup format.", "err");
      return;
    }

    // Write keys from backup
    const keys = Object.keys(payload.data);
    for (const k of keys) {
      await idbSet(k, payload.data[k]);
    }

    // Lock session because auth might have changed
    lock();

    // Refresh UI
    if (await authExists()) showLogin();
    else showSetup();

    setStatus("Backup restored. Now unlock using the backup password.", "ok");
  } catch (e) {
    setStatus("Restore failed: " + e.message, "err");
  }
}

// Fully wipe IndexedDB (panic button)
async function wipeDatabase() {
  try { if (db) db.close(); } catch {}

  await new Promise((resolve, reject) => {
    const req = indexedDB.deleteDatabase(DB_NAME);
    req.onsuccess = () => resolve(true);
    req.onerror = () => reject(req.error);
    req.onblocked = () => reject(new Error("Delete is blocked (close other tabs using this DB)."));
  });

  // Re-open fresh DB
  db = await openDB();

  // Reset runtime state
  session.unlocked = false;
  session.password = null;

  // Reset UI
  el("note").value = "";
  el("saveHint").textContent = "";
  showEditor(false);

  if (await authExists()) showLogin();
  else showSetup();

  setStatus("Database wiped.", "ok");
}

/* UI events */
el("btnSetup").addEventListener("click", async () => {
  const p1 = el("setupPass1").value;
  const p2 = el("setupPass2").value;
  if (!p1 || !p2) return setStatus("Enter the password twice.", "err");
  if (p1 !== p2) return setStatus("Passwords do not match.", "err");
  if (p1.length < 4) return setStatus("Password is too short (min 4 characters).", "err");

  await setPassword(p1);
  el("setupPass1").value = "";
  el("setupPass2").value = "";
  showLogin();
  setStatus("Password set. Please unlock.", "ok");
});

el("btnLogin").addEventListener("click", async () => {
  const p = el("loginPass").value;
  if (!p) return setStatus("Enter password.", "err");
  const ok = await unlock(p);
  if (ok) el("loginPass").value = "";
  else setStatus("Wrong password.", "err");
});

el("btnLogout").addEventListener("click", () => lock());

el("btnSave").addEventListener("click", async () => {
  try {
    await saveNote();
    setStatus("Saved.", "ok");
  } catch (e) {
    setStatus("Save error: " + e.message, "err");
  }
});

let typingTimer = null;
el("note").addEventListener("input", () => {
  if (!session.unlocked) return;
  clearTimeout(typingTimer);
  el("saveHint").textContent = "Unsaved changes…";
  typingTimer = setTimeout(() => {
    el("saveHint").textContent = "Unsaved changes… (click “Save”)";
  }, 500);
});

el("btnChangePass").addEventListener("click", async () => {
  await changePasswordFlow(
    el("oldPass").value,
    el("newPass1").value,
    el("newPass2").value
  );
});

el("btnBackup").addEventListener("click", async () => {
  try { await backup(); }
  catch (e) { setStatus("Backup error: " + e.message, "err"); }
});

el("btnRestore").addEventListener("click", () => el("fileRestore").click());
el("fileRestore").addEventListener("change", async (e) => {
  const file = e.target.files && e.target.files[0];
  e.target.value = "";
  if (!file) return;
  await restoreFromFile(file);
});

// Enter key support (more fields)
["loginPass"].forEach((id) => {
  el(id).addEventListener("keydown", (e) => {
    if (e.key === "Enter") el("btnLogin").click();
  });
});

["setupPass1", "setupPass2"].forEach((id) => {
  el(id).addEventListener("keydown", (e) => {
    if (e.key === "Enter") el("btnSetup").click();
  });
});

["oldPass", "newPass1", "newPass2"].forEach((id) => {
  el(id).addEventListener("keydown", (e) => {
    if (e.key === "Enter") el("btnChangePass").click();
  });
});

// Wipe DB with strong confirmation
el("btnWipe").addEventListener("click", async () => {
  const msg =
    "This will permanently delete ALL stored data (password hash + encrypted note).\n\n" +
    "Type DELETE to confirm:";
  const typed = prompt(msg);

  if (typed !== "DELETE") {
    setStatus("Wipe cancelled.", "muted");
    return;
  }

  try {
    await wipeDatabase();
  } catch (e) {
    setStatus("Wipe failed: " + e.message, "err");
  }
});

/* Init */
(async function init() {
  try {
    db = await openDB();
    const exists = await authExists();
    if (!exists) {
      showSetup();
      setStatus("First run: please set a password.", "muted", 0); // persistent
    } else {
      showLogin();
      setStatus("Enter password to unlock your note.", "muted", 0); // persistent
    }
    showEditor(false);
    showAuthCard(true);
  } catch (e) {
    setStatus("IndexedDB error: " + e.message, "err", 0);
  }
})();
</script>
</body>
</html>

Sort:  

I really enjoy your writing. Wishing you the best and continued success. Your job is really beautiful. ♥️

follow
@kfeedgamer5

756.jpg

Posted using SteemX