Frontend Labs
⋯

48. Mini project: an encrypted notes app (Web Crypto + IndexedDB)

Combining §45 (IndexedDB) and §46 (Web Crypto) into a zero-knowledge-style notes app: the server (or sync provider) only ever sees encrypted blobs.

class EncryptedNotesStore {
  #db; #key;

  async init(password) {
    this.#db = await openDB();   // from §45
    const salt = await this.#getOrCreateSalt();
    this.#key = await deriveKeyFromPassword(password, salt);   // from §46
  }

  async #getOrCreateSalt() {
    let salt = localStorage.getItem("salt");
    if (!salt) {
      const bytes = crypto.getRandomValues(new Uint8Array(16));
      salt = btoa(String.fromCharCode(...bytes));
      localStorage.setItem("salt", salt);   // salt is NOT secret, safe to store plainly
    }
    return Uint8Array.from(atob(salt), (c) => c.charCodeAt(0));
  }

  async saveNote(id, plaintext) {
    const { iv, ciphertext } = await encrypt(this.#key, plaintext);
    await addNote(this.#db, {
      id,
      iv: Array.from(iv),
      ciphertext: Array.from(new Uint8Array(ciphertext)),
      createdAt: Date.now(),
    });
  }

  async loadNote(record) {
    return decrypt(this.#key, {
      iv: new Uint8Array(record.iv),
      ciphertext: new Uint8Array(record.ciphertext).buffer,
    });
  }
}

// usage
const store = new EncryptedNotesStore();
await store.init("correct horse battery staple");
await store.saveNote("note-1", "Meet at the docks, midnight.");

If this synced ciphertext/iv/salt to a server, that server (or anyone who breaches it) would see only opaque bytes — but per §47, this app is still fully readable by any successful XSS against its own origin, since the derived key lives in that origin's memory the moment the user's password is entered. A production version would add a strict CSP (§51) as the actual defense against that scenario, not more crypto.


Generated from real-life-JS-guide.md · 8 Oct 2026 16:36