Encryptie Implementatie
Alle notities worden client-side met AES-256-GCM versleuteld. De server ziet nooit plaintext.
Algoritme
- Symmetrisch: AES-256-GCM (Galois/Counter Mode)
- Sleutel: 256-bit, random gegenereerd per cirkel
- IV (Initialisatievector): 12-byte, random per notitie
- API: Web Crypto API (native browser API)
Sleutel Management
Generatie (Bij Cirkel Aanmaken)
export async function genereerCirkelSleutel(): Promise<string> {
const sleutel = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, // exportable
['encrypt', 'decrypt']
);
const raw = await crypto.subtle.exportKey('raw', sleutel);
return btoa(String.fromCharCode(...new Uint8Array(raw)));
// Returns base64-encoded 256-bit key
}
Opslag
- Client: In-memory tijdens sessie
- Server: Opgeslagen in Supabase Vault (encrypted at rest)
- Vault key = master encryption key op server
- Cirkel-sleutel = versleuteld met master key
- Mentor kan sleutel ophalen (met RLS)
Vrijgave (Op Opening)
-- RLS Policy: cirkel_sleutel NULL tot opening_datum voorbij
CREATE POLICY "sleutel_pas_na_opening" ON cirkels
FOR SELECT USING (
CASE
WHEN opening_datum <= CURRENT_DATE THEN TRUE
ELSE (cirkel_sleutel IS NULL)
END
);
Encryption Flow
Notitie Schrijven
export async function versleutelNotitie(
tekst: string,
cirkelSleutelB64: string
): Promise<{ versleuteld: string; iv: string }> {
// 1. Decode sleutel van base64
const sleutelBytes = Uint8Array.from(
atob(cirkelSleutelB64),
c => c.charCodeAt(0)
);
// 2. Import als WebCrypto key
const sleutel = await crypto.subtle.importKey(
'raw',
sleutelBytes,
{ name: 'AES-GCM' },
false, // niet exportable
['encrypt']
);
// 3. Genereer random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// 4. Versleutel
const encoder = new TextEncoder();
const versleuteld = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
sleutel,
encoder.encode(tekst)
);
// 5. Return base64-encoded ciphertext + IV
return {
versleuteld: btoa(String.fromCharCode(...new Uint8Array(versleuteld))),
iv: btoa(String.fromCharCode(...iv))
};
}
Stuur naar server:
{
"cirkel_id": "uuid",
"ontvanger_id": "uuid",
"versleutelde_tekst": "base64-ciphertext",
"iv": "base64-iv"
}
Server slaat op zonder ooit plaintext te zien.
Notitie Lezen (Na Opening)
export async function ontsleutelNotitie(
versleuteldB64: string,
ivB64: string,
cirkelSleutelB64: string
): Promise<string> {
// 1. Decode sleutel
const sleutelBytes = Uint8Array.from(
atob(cirkelSleutelB64),
c => c.charCodeAt(0)
);
// 2. Import
const sleutel = await crypto.subtle.importKey(
'raw',
sleutelBytes,
{ name: 'AES-GCM' },
false,
['decrypt']
);
// 3. Decode IV + ciphertext
const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(versleuteldB64), c => c.charCodeAt(0));
// 4. Ontsleutel
const ontsleuteld = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
sleutel,
data
);
return new TextDecoder().decode(ontsleuteld);
}
Security Properties
Wat De Server Beschermt
✅ Server kan nooit notitie-inhoud lezen ✅ Zelfs admin van Supabase kan niet decrypten ✅ Ciphertext is onbruikbaar zonder sleutel ✅ IV is uniek per notitie (voorkomt pattern analysis)
Wat De Server WEL Controleert
✅ RLS blokkeert sleutel tot opening_datum ✅ Cron job ontsleutelt op exacte datum ✅ Notities kunnen niet worden gewijzigd (immutable) ✅ Audit log van alles
Beperking
⚠️ Gebruiker MOET op opening_datum de pagina bezoeken ⚠️ Sleutel is in-memory, niet in localStorage (beter, maar minder persistentie) ⚠️ Ontsleuteling gebeurt in browser — plaintext even in RAM
Testing Encryptie
// Test encrypt → decrypt
const tekst = "Geheim bericht";
const sleutel = await genereerCirkelSleutel();
const { versleuteld, iv } = await versleutelNotitie(tekst, sleutel);
const decrypted = await ontsleutelNotitie(versleuteld, iv, sleutel);
console.assert(decrypted === tekst, "Encrypt/decrypt failed");
Vault (Server-Side)
Supabase Vault beschermt de cirkel-sleutel:
// In Edge Function (server-side only)
const { data: vault } = await supabase
.from('cirkels')
.select('cirkel_sleutel')
.eq('id', cirkelId)
.eq('opening_datum', today)
.single();
// vault.cirkel_sleutel wordt geONTSLEUTELD door Vault
// Stuur naar client
AES-256-GCM Details
| Eigenschap | Waarde |
|---|---|
| Algoritme | AES-256-GCM |
| Sleutelgrootte | 256 bits (32 bytes) |
| IV-grootte | 96 bits (12 bytes) |
| Authenticatie | GCM (Galois/Counter Mode) |
| Padding | Geen (GCM handelt dit af) |
Waarom GCM?
- Versleuteling + authenticatie in één
- Detect wijzigingen aan ciphertext
- Geen padding oracle attacks