Öffentliche Architekturdoku
Wie dein Tresor verschlüsselt
Auf dieser Seite siehst du, was in deinem Browser passiert, bevor Dateien deinen Rechner verlassen. Du musst uns nicht blind vertrauen: die entscheidende Verschlüsselung läuft bei dir lokal, der Quellcode der Client-Logik ist einsehbar.
- Hier dokumentiert: Schlüsselableitung, Dateiverschlüsselung, Dateiindex & Sync, öffentliche API-Schnittstellen
- Nur Überblick: interne Server-Implementierung und Betriebsdetails
- Nie über das Netz: dein Tresor-Passwort, deine Recovery-Phrase, der unverschlüsselte Master-Schlüssel, unverschlüsselte Dateiinhalte
- Auf dem Server: Salts, verschlüsselte Hüllen (
passwordWrapped,recoveryWrapped) und ein separater Recovery-Proof-Key, der nur Passwort-Reset-Challenges signieren kann
Zero-Knowledge in einem Satz
Der Server speichert nur verschlüsselte Dateiinhalte und technische Hüllen (Salts, Wrapped Keys). Dein Master-Schlüssel entsteht beim ersten Setup in deinem Browser und wird dort in Hüllen verpackt, bevor etwas an den Server geht. Beim Entsperren wird er ausschließlich lokal wieder freigegeben. Ohne dein Passwort oder deine Recovery-Phrase können Inhalte nicht gelesen werden, auch nicht durch den Betreiber.
Ersteinrichtung deines Tresors
Beim ersten Öffnen legst du Passwort und Recovery-Phrase fest. Intern passiert Folgendes:
- Du wählst ein Passwort (mindestens 12 Zeichen).
- Der Browser erzeugt zufällige Salts und würfelt 32 Master-Bytes per CSPRNG (
generateMasterBits()). - Diese Bytes werden unter deiner Recovery-Phrase versiegelt (Wrap).
- Dieselben Bytes werden unter deinem Passwort separat versiegelt.
- Nur die Hüllen und Salts werden an den Server gesendet: nie das Passwort, nie die Recovery-Wörter, nie der Master-Schlüssel im Klartext.
passwordWrapped ist dabei nicht dein Passwort in verschlüsselter Form, sondern der Master-Schlüssel (32 Byte), mit einem vom Passwort abgeleiteten Schlüssel versiegelt.
export async function wrapMasterBits(masterBits, kek) {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ct = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, kek, masterBits
);
return { wrappedHex: toHex(ct), ivHex: toHex(iv) };
}
// Beim Setup sendet der Client u. a.:
// salt, recoverySalt, recoveryWrapped, recoveryIv, recoveryProofKey,
// passwordWrapped, passwordIvSchlüsselerzeugung
Beim ersten Setup würfelt der Browser 32 zufällige Master-Bytes (CSPRNG). Aus deinem Tresor-Passwort wird per PBKDF2-SHA256 mit 600.000 Iterationen nur der Schlüssel abgeleitet, der diese Bytes in passwordWrapped versiegelt. Beim Entsperrenleitet der Browser aus deinem Passwort einen KEK ab, um die Hülle lokal zu öffnen. Der Session-Schlüssel ist extractable: false und verlässt den Arbeitsspeicher nicht in lesbarer Form.
export function generateMasterBits() {
return globalThis.crypto.getRandomValues(new Uint8Array(32));
}
const PBKDF2_ITERATIONS = 600000;
export async function deriveKeyFromPassword(password, salt) {
const enc = new TextEncoder();
const keyMaterial = await window.crypto.subtle.importKey(
"raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits", "deriveKey"]
);
return await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: enc.encode(salt),
iterations: PBKDF2_ITERATIONS,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false, // extractable: false, Schlüssel bleibt im RAM
["encrypt", "decrypt"]
);
}Passwort-Verifikation & Recovery-Phrase
Dein Tresor-Passwort wird weder gespeichert noch an den Server übertragen. Der Server erhält und behält nur eine verschlüsselte Hülle: den Master-Schlüssel, verpackt mit AES-GCM unter einem Schlüssel, der aus deinem Passwort und einem Salt abgeleitet wird. Erst wenn du im Browser dein Passwort eingibst, kann diese Hülle geöffnet und der Master-Schlüssel im RAM genutzt werden.
| Daten | Geht an den Server? | Liegt auf dem Server? |
|---|---|---|
| Tresor-Passwort (Klartext) | Nein | Nein |
| Recovery-Phrase (Klartext) | Nein | Nein |
| Master-Schlüssel (Klartext) | Nein | Nein |
salt (Passwort-Kanal) | Ja (beim Setup) | Ja (öffentlich, kein Geheimnis) |
passwordWrapped + passwordIv | Ja (beim Setup) | Ja (verschlüsselter Master-Key) |
recoveryWrapped + recoveryIv + recoverySalt | Ja (beim Setup) | Ja (verschlüsselter Master-Key, Recovery-Kanal) |
recoveryProofKey | Ja (beim Setup) | Ja (domain-separierter Proof-Key für Reset-Challenges, kein Dateischlüssel) |
Ersteinrichtung (Setup)
- Du gibst dein Passwort ein (bleibt im Browser).
- Der Browser erzeugt
salt, würfelt 32 Master-Bytes (CSPRNG) und leitet den KEK aus dem Passwort ab (PBKDF2). - Diese Bytes werden mit einem passwort-abgeleiteten Schlüssel versiegelt →
passwordWrapped. - Parallel dasselbe für die Recovery-Phrase →
recoveryWrapped. - Salts, Hüllen und ein separater Proof-Key für Recovery-Reset-Challenges gehen an
POST /api/tresor/setup.
Entsperren (Unlock)
- Der Browser lädt
salt,passwordWrappedundpasswordIvvom Server. - Du gibst dein Passwort ein (nur lokal, nie im Request).
- Daraus wird ein Schlüssel abgeleitet, die Hülle entschlüsselt, der Master-Key importiert.
- Erst jetzt kann der Tresor Dateien entschlüsseln. Falsches Passwort → AES-GCM schlägt fehl, kein Fallback.
export async function unlockWithPassword(password, salt, passwordWrapped, passwordIv) {
if (!passwordWrapped || !passwordIv) {
throw new Error('PASSWORD_ENVELOPE_MISSING');
}
// password bleibt im Browser; Server sieht es nie
const kek = await deriveKekFromPassword(password, salt);
const masterBits = await unwrapMasterBits(passwordWrapped, passwordIv, kek);
return importMasterKeyFromBits(masterBits); // sessionKey, nur RAM
}Deine 12-Wörter Recovery-Phrase folgt BIP39 (128 Bit Entropie + Prüfsumme) und funktioniert über einen separaten Kanal (recoverySalt, recoveryWrapped). Auch hier gilt: die Wörter verlassen deinen Browser nicht; auf dem Server liegen nur die verschlüsselte Hülle und ein daraus domain-separiert abgeleiteter Proof-Key für Reset-Challenges.
export async function generateMnemonic() {
const entropy = window.crypto.getRandomValues(new Uint8Array(16));
const hash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', entropy));
// 128 Bit Entropie + 4 Bit Checksumme → 12 Wörter aus BIP39-Wortliste
return words.join(' ');
}Stellt die Recovery-Phrase mein Passwort wieder her?
Nein. Dein Tresor-Passwort wird nirgends gespeichert, weder im Klartext noch verschlüsselt. Die Recovery-Phrase ist kein „Passwort-Knacker“, sondern ein zweiter Schlüssel zum gleichen Master-Schlüssel. Beim Setup werden dieselben 32 Master-Bytes einmal unter deinem Passwort und einmal unter deiner Phrase verpackt (passwordWrapped und recoveryWrapped). Wer die Phrase kennt, kann den Tresor öffnen, genauso wie mit dem Passwort, aber daraus lässt sich dein Passwort nicht rekonstruieren.
Passwort zurücksetzen (mit Recovery-Phrase)
Wenn du dein Passwort vergessen hast, aber deine 12 Wörter noch hast, kannst du ein neues Passwort setzen, ohne deine Dateien neu zu verschlüsseln:
- Du gibst die Recovery-Phrase lokal ein (sie verlässt den Browser nicht).
- Der Browser entschlüsselt
recoveryWrapped→ du erhältst wieder den gleichen Master-Schlüssel. - Du wählst ein neues Passwort; daraus wird ein neuer KEK abgeleitet.
- Derselbe Master-Schlüssel wird unter dem neuen Passwort neu verpackt; zusätzlich signiert der Browser eine serverseitige Challenge mit dem Recovery-Proof-Key.
- Neues
passwordWrappedplus Challenge-Proof gehen anPOST /api/tresor/reset-password.
Entsperren, Sperren, Session
Nach dem Entsperren liegt dein Master-Schlüssel nur im Arbeitsspeicher der Browser-Sitzung (sessionKey). Er wird weder in localStorage noch in IndexedDB abgelegt. Vorher lag er ausschließlich als verschlüsselte Hülle auf dem Server; freigegeben wird er erst durch deine lokale Passwort- oder Recovery-Eingabe (siehe Abschnitt Passwort & Recovery).
Wenn du „Tresor sperren“ wählst, wird der Schlüssel aus dem RAM entfernt und die Dateiliste ausgeblendet. Dein Datenputzer-Login bleibt bestehen, du musst dich nicht erneut bei datenputzer.de anmelden.
Zwei Ebenen sind zu unterscheiden:
- Datenputzer-Login (HttpOnly-Cookie): Zugang zum Konto und Tarif
- Tresor entsperrt (Schlüssel im RAM): Zugriff auf verschlüsselte Inhalte
Optional kannst du einen Passkey (Touch ID / Windows Hello) registrieren. Das ersetzt weder Passwort noch Recovery-Phrase, sondern bietet einen schnelleren Entsperrweg mit Hardware-Schutz (PRF-Envelope).
RAM und der Edge-Vergleich
Im Mai 2026 wurde bekannt, dass Microsoft Edge gespeicherte Passwörter beim Browser-Start entschlüsselt und dauerhaft im Arbeitsspeicher hielt. Der Tresor verhält sich grundlegend anders:
| Verhalten | Edge (Problem) | Datenputzer Tresor |
|---|---|---|
| Wann wird entschlüsselt? | Beim Browser-Start, ohne deine Eingabe | Erst wenn du Passwort, Recovery oder Passkey eingibst |
| Wie lange liegt der Schlüssel im RAM? | Ganze Session | Nur bis du sperrst, Auto-Sperre greift oder Tab-Sperre aktiv ist |
| Schlüssel auf Disk? | Passwort-Manager-Datenbank (verschlüsselt, aber beim Start offen) | Nur Hüllen (passwordWrapped, recoveryWrapped); Master-Key nie persistiert |
| Hardware-Gate | OS-PIN für Manager, nicht pro Entsperrung | Optional Passkey mit TPM/Secure Enclave (PRF) |
Threat Model (deine Perspektive)
Worauf du dich verlassen kannst und wo du selbst handeln musst:
| Bedrohung | Schutz im Tresor | Deine Maßnahme |
|---|---|---|
| Server liest Dateiinhalte | Zero-Knowledge: nur Ciphertext | Recovery-Phrase sicher aufbewahren |
| Abhörung im Netz | TLS + bereits verschlüsselte Chunks | Vertrauenswürdiges Gerät nutzen |
| Malware auf dem PC | Teilweise: Auto-Sperre, Passkey-Gate | Tresor sperren, System absichern |
| XSS im Tresor-Origin | CSP, kein Inline-Script, Crypto im Worker | Browser aktuell halten |
| Physischer Zugriff | Sperre, optional Tab-Sperre | Kurze Auto-Sperre, Gerät verriegeln |
| Passkey-Verlust | Passwort + Recovery bleiben | Recovery-Phrase notieren, Passkey nicht als einzigen Weg |
Einstellungen für mehr Sicherheit
Unter Einstellungen → Sicherheit im Tresor kannst du folgendes konfigurieren:
- Automatisch sperren nach … (5 / 15 / 30 / 60 Minuten oder Aus): Bei Inaktivität wird der Master-Schlüssel aus dem RAM entfernt. Upload-Aktivität zählt als Nutzung.
- Sperren während Uploads: „Laufende Uploads beenden“ hält den Schlüssel kurz für den aktiven Job; „Sofort stoppen (streng)“ löscht ihn sofort und pausiert die Queue.
- Sperren wenn Tab im Hintergrund: Optional, standardmäßig aus. Sperrt beim Tab-Wechsel (Paranoia-Modus).
- Passkey (optional): Touch ID / Windows Hello zum Entsperren. Erfordert einmal Passwort-Eingabe zur Registrierung. PRF-Support variiert je nach Browser und Authenticator.
tresor_auto_lock_minutes // 0 | 5 | 15 | 30 | 60
tresor_lock_upload_policy // strict | finish_current
tresor_lock_on_tab_hidden // 0 | 1Dateiverschlüsselung (AES-GCM)
Deine Dateien werden in 5-MiB-Chunks geteilt. Jeder Chunk erhält eine frische 12-Byte-IV. Zusätzliche authentifizierte Daten (AAD) binden den Ciphertext an eine Datei-UUID und den Chunk-Index, damit Stücke nicht vertauscht werden können.
export async function encryptChunk(arrayBuffer, key, fileId, chunkIndex) {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const aad = new TextEncoder().encode(`${fileId}:${chunkIndex}`);
const encryptedContent = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData: aad },
key,
arrayBuffer
);
const combined = new Uint8Array(iv.length + encryptedContent.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encryptedContent), iv.length);
return combined;
}Download und Entschlüsselung
Klickst du eine Datei an, lädt der Browser die verschlüsselten Chunks und entschlüsselt sie mit deinem Session-Schlüssel. Für große Dateien nutzt der Tresor Streaming-Entschlüsselung: Klartext-Chunks werden nacheinander erzeugt, nicht alle gleichzeitig im RAM gehalten.
export async function decryptBlobStream(encryptedBlob, key, originalType, fileId) {
const combinedArray = new Uint8Array(await encryptedBlob.arrayBuffer());
const stream = new ReadableStream({
async pull(controller) {
// Chunkweise decryptChunk → enqueue → nächster Chunk
// Kein plaintextChunks[] mit der ganzen Datei
}
});
return new Blob([stream], { type: originalType });
}Scheitert die Entschlüsselung (falsches Passwort, beschädigte Daten, manipulierter Chunk), bricht der Vorgang mit einer Fehlermeldung ab.
Dateiindex: lokal und in der Cloud
Dein Dateiindex (Ordner, Dateinamen, Struktur) existiert auf zwei Ebenen: schnell lokal in IndexedDB und verschlüsselt synchronisiert auf dem Server im Änderungsjournal.
Lokal: IndexedDB (Dexie)
Nach dem Entsperren liegen Ordner und Dateimetadaten in Dexie/IndexedDB unter TresorDatabase auf deinem Gerät. So kannst du sortieren, suchen und Dateien auch offline öffnen, ohne bei jedem Klick den Server zu fragen.
db.version(5).stores({
folders: '++id, clientId, parentClientId, parentId, name, createdAt',
files: '++id, clientId, folderClientId, folderId, name, encryptionId, uploadId, ...',
fileBlobs: 'id',
pendingUploads: 'id, fileName, uploadId, uploadedChunks, totalChunks',
meta: 'key' // u. a. journalLastEventId
});files: Name, Ordner, Größe,encryptionId(UUID für AAD),uploadIdfür Cloud-ChunksfileBlobs/pendingFileBlobs: Zwischenspeicher für unterbrochene Uploads (nur Ciphertext)- Maximal 64 Ordner pro Tresor (
MAX_VAULT_FOLDERS)
Cloud: verschlüsseltes Journal
Für Geräteübergreifenden Sync schreibt der Browser jede Änderung (Ordner anlegen, Datei hinzufügen, umbenennen, verschieben, löschen) als Event ins Journal. Das Event wird mit dem Master-Schlüssel per AES-GCM verschlüsselt und als encrypted_payload an POST /api/tresor/journal gesendet. Auf dem Server liegt nur der Ciphertext in journal_events, keine Klartext-Dateinamen.
- Beim Entsperren lädt
syncJournalFromServer()neue Events (GET /api/tresor/journal?since=…). - Jedes Event wird im Browser mit dem Master-Schlüssel entschlüsselt.
- Die Klartext-Metadaten werden in IndexedDB übernommen (
applyJournalEvent).
const encrypted_payload = await encryptJournalPayload(masterKey, {
type: 'file_add', name: '…', parentClientId: '…', uploadId: '…', …
});
// → POST /api/tresor/journal
const event = await decryptJournalPayload(masterKey, row.encrypted_payload);
await applyJournalEvent(event); // → IndexedDB files/foldersUpload
- Du wählst oder ziehst eine Datei in den Tresor.
- Der Browser teilt sie in 5-MiB-Stücke und verschlüsselt jedes Stück.
- Du siehst Fortschritt: Vorbereiten → Verschlüsseln → Übertragen → Abschließen
- Die Ciphertext-Chunks gehen an
/api/tresor/upload/chunk; nachupload/completeist die Datei gesichert.
export const CHUNK_SIZE = 5 * 1024 * 1024;
const chunkBuffer = await file.slice(sliceStart, sliceEnd).arrayBuffer();
const encryptedChunk = await encryptChunk(chunkBuffer, activeKey, fileUuid, c);
await uploadChunkWithRetry(authenticatedFetch, uploadId, c, encryptedChunk);Welche Metadaten sichtbar sind
Zero-Knowledge schützt Inhalte und Schlüssel, nicht automatisch alle Metadaten. Das solltest du bei deiner Risikoabwägung kennen:
| Information | Verschlüsselt? | Wo sichtbar? |
|---|---|---|
| Dateiinhalt (PDF, Bild, …) | Ja | Nur bei dir nach Entschlüsselung |
| Master-Schlüssel, Passwort, Recovery | N/A | Nur kurz im RAM nach Entsperren |
| Dateiname, Ordnername | Ja (im Journal) | Verschlüsselt im Änderungsjournal auf dem Server; Klartext nur lokal nach Entsperren |
| Dateigröße, Zeitstempel | Nein | Lokal; Quota auf dem Server |
| Tarif, Speicherbelegung | Nein | Server (für Abrechnung) |
| Login-Session | Nein | HttpOnly-Cookie deines Browsers |
Kryptografische Parameter (Referenz)
| Parameter | Wert |
|---|---|
| Passwort-Ableitung | PBKDF2-SHA256, 600.000 Iterationen |
| Verschlüsselung | AES-256-GCM |
| IV pro Chunk | 12 Byte, zufällig (Web Crypto) |
| AAD-Bindung | fileUuid + Chunk-Index |
| Chunk-Größe (Klartext) | 5 MiB |
| Recovery-Phrase | BIP39, 12 Wörter (128 Bit + Checksumme) |
| Max. Ordner | 64 pro Tresor |
| Session-Schlüssel | Nicht exportierbar, nur RAM |
Warum PBKDF2 und nicht Argon2?
Argon2id ist heute der Goldstandard für Passwort-KDFs. Wir nutzen bewusst PBKDF2-HMAC-SHA256 mit 600.000 Iterationen im Browser:
- Die native Web-Crypto-API (
crypto.subtle) unterstützt kein Argon2id. - Eine WASM-Library würde eine zusätzliche Supply-Chain-Angriffsfläche im Zero-Knowledge-Kontext eröffnen.
- PBKDF2 + per-User-Salt + AES-GCM-Envelope ist bei Passwörtern ab 12 Zeichen ausreichend stark.
- Die Wahl folgt NIST SP 800-132 und OWASP-Empfehlungen für PBKDF2-Iterationen.
Server (Überblick)
Hinter /api/tresor/* läuft ein separater Dienst. Er prüft deine Datenputzer-Session, verwaltet Tarif und Quota und nimmt nur bereits verschlüsselte Chunks entgegen.
| Endpoint | Zweck für dich |
|---|---|
GET /api/tresor/status | Tarif, Quota, Salts, Key-Hüllen |
POST /api/tresor/setup | Ersteinrichtung |
GET /api/tresor/recovery-info | Recovery-Hülle zum Entsperren |
POST /api/tresor/reset-password | Neues Passwort nach Recovery |
POST /api/tresor/webauthn/* | Passkey registrieren / entsperren (optional) |
POST /api/tresor/upload/init | Upload-Session starten |
POST /api/tresor/upload/chunk | Ciphertext-Chunk senden |
POST /api/tresor/upload/complete | Upload abschließen |
POST /api/tresor/upload/abort | Upload abbrechen, Quota freigeben |
DELETE /api/tresor/upload/:uploadId | Verschlüsselte Chunks löschen, Quota freigeben |
So kannst du es selbst prüfen
Du brauchst kein Spezialwerkzeug, nur die Entwicklertools deines Browsers (F12):
- Network: Lade eine Datei hoch. In den Requests solltest du keinen lesbaren PDF- oder Bildinhalt sehen, sondern binäre oder undurchsichtige Payloads.
- Sources: Suche im ausgelieferten JavaScript nach
PBKDF2_ITERATIONSoderencryptChunkund vergleiche mit den Snippets auf dieser Seite. - Application → IndexedDB → TresorDatabase: Unter
fileBlobssiehst du nur verschlüsselte Daten, keine Klartext-Dateien. - Passworttest: Ein falsches Passwort muss beim Entsperren scheitern, solange eine gültige
passwordWrapped-Hülle hinterlegt ist.
Hinweis: Produktionsbuilds sind minifiziert. Die Logik entspricht den Modulen crypto.js, db.js und uploads.js im Frontend-Quellcode.
Grenzen und Risiken
Häufige Fragen
Kann der Server mein Passwort aus der Recovery-Phrase ableiten?
Nein. Passwort und Phrase sind getrennte Kanäle zu demselben Master-Schlüssel. Der Server hat nur verschlüsselte Hüllen, keine der beiden Geheimnisse.
Verliere ich meine Dateien, wenn ich nur das Passwort zurücksetze?
Nein, solange du die Recovery-Phrase hast und der Master-Schlüssel unverändert bleibt. Es wird nur die Passwort-Hülle erneuert.
Sieht der Server meine Dateinamen?
Nicht im Klartext. Sie stecken verschlüsselt im Journal. Sichtbar bleiben u. a. Speicherbelegung, Upload-IDs und Chunk-Anzahlen (siehe Metadaten).
- Dein Gerät: Schadsoftware oder Browser-Erweiterungen könnten beim Entsperren theoretisch auf den Arbeitsspeicher zugreifen. Halte dein System und deine Extensions sauber.
- Verlorenes Passwort + verlorene Recovery-Phrase: Deine Daten sind unwiderruflich weg. Das ist bei echter Zero-Knowledge-Verschlüsselung so gewollt.
- XSS und Supply Chain: Der Tresor setzt eine strenge Content-Security-Policy. Trotzdem solltest du wie bei jeder Web-App auf vertrauenswürdige Browser und Updates achten.
- Kein File-Sharing: Der Datenputzer Tresor ist bewusst ein persönlicher Zero-Knowledge-Speicher für einen Nutzer. Es gibt keine Public-Key-Infrastruktur für Empfänger, Sharing ist nicht implementiert und nicht geplant.
- Kein Zugriff auf fremde Konten: Diese Seite beschreibt den Client. Sie ist kein Angriffsleitfaden und enthält keine geheimen Server-Internals.
Hinweise oder Anregungen: support@datenputzer.de