← Terug naar tamplat.nl

Een dagboek dat niemand anders kan lezen

Journalist is een journaling-app waarbij zelfs ik, als bouwer, je entries niet kan inzien. De app is afgeschermd met login, dus hier loop ik je stap voor stap door wat er gebeurt.

Waarom uitleg in plaats van een demo

De app staat achter een account

Journalist vraagt om een account, en daarna nog een keer om een wachtwoord om je dagboek te ontgrendelen. Dat is geen bug, dat is het hele punt. Omdat ik geen live rondleiding kan geven zonder bij iemands dagboek te kunnen, leg ik hier uit hoe het werkt aan de hand van screenshots en de daadwerkelijke code.

Twee sloten, niet één

Inloggen en ontgrendelen zijn niet hetzelfde

Er zit een belangrijk onderscheid in de app dat in eerste instantie verwarrend kan lijken: je logt in op je account, en daarna ontgrendel je apart je "vault" (kluis) met een wachtwoord. Dat zijn bewust twee verschillende stappen.

Inloggen bewijst aan de server wie je bent. Het ontgrendelen van de vault gebeurt volledig in je browser en levert de sleutel op waarmee je entries worden versleuteld en ontsleuteld. De server ziet die sleutel nooit. Zelfs met volledige toegang tot de database kan niemand je entries lezen zonder dat ontgrendel-wachtwoord.

Dit heet een zero-knowledge opzet: de server bewaart alleen versleutelde data en weet zelf niets over de inhoud.

Diagram 1

Inloggen op je account

Dit is de gewone authenticatie, zoals bij elke andere site. Niets bijzonders, maar wel de eerste stap voordat de interessante versleuteling begint.

sequenceDiagram
    participant U as Gebruiker
    participant F as Browser (Nuxt)
    participant A as Auth API
    participant DB as Database

    U->>F: Vult username + wachtwoord in
    F->>A: POST /api/auth/login
    A->>DB: Zoek gebruiker op username
    DB-->>A: Gebruikersrecord (incl. wachtwoord-hash)
    A->>A: Vergelijk wachtwoord met hash
    A-->>F: Sessie aangemaakt
    F-->>U: Redirect naar workspace
      

Diagram 2

De vault ontgrendelen en een entry lezen

Dit is waar het echte werk gebeurt, en waarom de server nooit je wachtwoord of je sleutel ziet. Het wachtwoord wordt via Argon2id omgezet in een sleutel (de KEK), die op zijn beurt de eigenlijke versleutelingssleutel (de DEK) ontgrendelt. Die DEK leeft alleen in het geheugen van je browser, en verdwijnt zodra je de pagina herlaadt.

sequenceDiagram
    participant U as Gebruiker
    participant F as Browser (Nuxt)
    participant A as Vault API
    participant DB as Database

    U->>F: Vult vault-wachtwoord in
    F->>A: GET /api/vault/unlock-data
    A->>DB: Haal salt + wrappedDEK op
    DB-->>A: salt, wrappedDEK
    A-->>F: salt, wrappedDEK
    F->>F: Argon2id(wachtwoord, salt) → KEK
    F->>F: unwrapKey(wrappedDEK, KEK) → DEK
    Note over F: DEK leeft alleen in geheugen,
verdwijnt bij refresh F-->>U: Vault ontgrendeld U->>F: Open een entry F->>A: GET /api/entries/:id A->>DB: Haal versleutelde entry op DB-->>A: encryptedBody, iv A-->>F: encryptedBody, iv F->>F: AES-GCM decrypt met DEK F-->>U: Leesbare entry

Bij het opslaan gebeurt precies het omgekeerde: de tekst wordt lokaal versleuteld met AES-GCM voordat hij naar de server gaat. De server slaat alleen encryptedBody en de iv op, nooit de platte tekst.

In de praktijk

Hoe dat eruit ziet

Stap 1
Workspace overzicht met Journal, Tasks, Pomodoro en meer
Na het inloggen kom je in de workspace. Journal is nu actief, de rest (Tasks, Pomodoro, Analytics) staat nog op slot, want work in progress.
Stap 2
Inlogscherm van de workspace
Het inlogscherm zelf. Dit is de gewone account-authenticatie uit diagram 1.
Stap 3
Vault ontgrendelen met wachtwoord
Na het inloggen volgt het echte slot: de vault. Hier wordt het wachtwoord lokaal omgezet in de sleutel die je entries ontsleutelt, zoals in diagram 2.
Stap 4
Een geopende journal entry met tekst-opmaak
Eenmaal ontgrendeld, gewoon een entry openen en schrijven. Opmaak zoals headers, bold, code-blokken en emoji's werkt al. Afbeeldingen in entries staan nog op de planning.

Onder de motorkap

Tech stack

LaagKeuze
FrameworkNuxt 3 (Vue 3 + TypeScript)
DatabasePostgreSQL
ORMDrizzle
Wachtwoord-validatienuxt-auth-utils (sessie-gebaseerd)
Key derivationArgon2id (hash-wasm)
Sleutel-wrappingAES-KW (Web Crypto API)
Entry-encryptieAES-GCM, client-side

De DEK (de sleutel die entries versleutelt) wordt nooit naar de server gestuurd, ook niet versleuteld. Alleen de wrapped versie staat in de database, en die is zonder wachtwoord onbruikbaar.