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.
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
Onder de motorkap
Tech stack
| Laag | Keuze |
|---|---|
| Framework | Nuxt 3 (Vue 3 + TypeScript) |
| Database | PostgreSQL |
| ORM | Drizzle |
| Wachtwoord-validatie | nuxt-auth-utils (sessie-gebaseerd) |
| Key derivation | Argon2id (hash-wasm) |
| Sleutel-wrapping | AES-KW (Web Crypto API) |
| Entry-encryptie | AES-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.