Ö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.

1 Du gibst Passwort oder Recovery ein (nur lokal)
2 Browser entschlüsselt die Hülle → Master-Key im RAM
3 AES-GCM lokal für Dateien
4 Nur Ciphertext geht raus

Ersteinrichtung deines Tresors

Beim ersten Öffnen legst du Passwort und Recovery-Phrase fest. Intern passiert Folgendes:

  1. Du wählst ein Passwort (mindestens 12 Zeichen).
  2. Der Browser erzeugt zufällige Salts und würfelt 32 Master-Bytes per CSPRNG (generateMasterBits()).
  3. Diese Bytes werden unter deiner Recovery-Phrase versiegelt (Wrap).
  4. Dieselben Bytes werden unter deinem Passwort separat versiegelt.
  5. 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.

src/crypto.js · Master-Key unter Recovery versiegeln
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, passwordIv

Schlü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.

src/crypto.js · Master-Key erzeugen & KEK ableiten
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.

DatenGeht an den Server?Liegt auf dem Server?
Tresor-Passwort (Klartext)NeinNein
Recovery-Phrase (Klartext)NeinNein
Master-Schlüssel (Klartext)NeinNein
salt (Passwort-Kanal)Ja (beim Setup)Ja (öffentlich, kein Geheimnis)
passwordWrapped + passwordIvJa (beim Setup)Ja (verschlüsselter Master-Key)
recoveryWrapped + recoveryIv + recoverySaltJa (beim Setup)Ja (verschlüsselter Master-Key, Recovery-Kanal)
recoveryProofKeyJa (beim Setup)Ja (domain-separierter Proof-Key für Reset-Challenges, kein Dateischlüssel)

Ersteinrichtung (Setup)

  1. Du gibst dein Passwort ein (bleibt im Browser).
  2. Der Browser erzeugt salt, würfelt 32 Master-Bytes (CSPRNG) und leitet den KEK aus dem Passwort ab (PBKDF2).
  3. Diese Bytes werden mit einem passwort-abgeleiteten Schlüssel versiegelt → passwordWrapped.
  4. Parallel dasselbe für die Recovery-Phrase → recoveryWrapped.
  5. Salts, Hüllen und ein separater Proof-Key für Recovery-Reset-Challenges gehen an POST /api/tresor/setup.

Entsperren (Unlock)

  1. Der Browser lädt salt, passwordWrapped und passwordIv vom Server.
  2. Du gibst dein Passwort ein (nur lokal, nie im Request).
  3. Daraus wird ein Schlüssel abgeleitet, die Hülle entschlüsselt, der Master-Key importiert.
  4. Erst jetzt kann der Tresor Dateien entschlüsseln. Falsches Passwort → AES-GCM schlägt fehl, kein Fallback.
src/crypto.js · Entsperren per Passwort (vollständig lokal)
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.

src/crypto.js · Recovery-Mnemonic erzeugen (Auszug)
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:

  1. Du gibst die Recovery-Phrase lokal ein (sie verlässt den Browser nicht).
  2. Der Browser entschlüsselt recoveryWrapped → du erhältst wieder den gleichen Master-Schlüssel.
  3. Du wählst ein neues Passwort; daraus wird ein neuer KEK abgeleitet.
  4. Derselbe Master-Schlüssel wird unter dem neuen Passwort neu verpackt; zusätzlich signiert der Browser eine serverseitige Challenge mit dem Recovery-Proof-Key.
  5. Neues passwordWrapped plus Challenge-Proof gehen an POST /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:

VerhaltenEdge (Problem)Datenputzer Tresor
Wann wird entschlüsselt?Beim Browser-Start, ohne deine EingabeErst wenn du Passwort, Recovery oder Passkey eingibst
Wie lange liegt der Schlüssel im RAM?Ganze SessionNur 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-GateOS-PIN für Manager, nicht pro EntsperrungOptional Passkey mit TPM/Secure Enclave (PRF)

Threat Model (deine Perspektive)

Worauf du dich verlassen kannst und wo du selbst handeln musst:

BedrohungSchutz im TresorDeine Maßnahme
Server liest DateiinhalteZero-Knowledge: nur CiphertextRecovery-Phrase sicher aufbewahren
Abhörung im NetzTLS + bereits verschlüsselte ChunksVertrauenswürdiges Gerät nutzen
Malware auf dem PCTeilweise: Auto-Sperre, Passkey-GateTresor sperren, System absichern
XSS im Tresor-OriginCSP, kein Inline-Script, Crypto im WorkerBrowser aktuell halten
Physischer ZugriffSperre, optional Tab-SperreKurze Auto-Sperre, Gerät verriegeln
Passkey-VerlustPasswort + Recovery bleibenRecovery-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.
src/securityPrefs.js · localStorage-Schlüssel
tresor_auto_lock_minutes      // 0 | 5 | 15 | 30 | 60
tresor_lock_upload_policy      // strict | finish_current
tresor_lock_on_tab_hidden      // 0 | 1

Dateiverschlü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.

src/crypto.js · Ein Chunk verschlüsseln
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.

src/crypto.js · Streaming-Entschlüsselung (Auszug)
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.

src/db.js · Schema (v5, Auszug)
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), uploadId für Cloud-Chunks
  • fileBlobs / 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.

  1. Beim Entsperren lädt syncJournalFromServer() neue Events (GET /api/tresor/journal?since=…).
  2. Jedes Event wird im Browser mit dem Master-Schlüssel entschlüsselt.
  3. Die Klartext-Metadaten werden in IndexedDB übernommen (applyJournalEvent).
src/journal.js · Event verschlüsseln und syncen (Auszug)
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/folders

Upload

  1. Du wählst oder ziehst eine Datei in den Tresor.
  2. Der Browser teilt sie in 5-MiB-Stücke und verschlüsselt jedes Stück.
  3. Du siehst Fortschritt: Vorbereiten → Verschlüsseln → Übertragen → Abschließen
  4. Die Ciphertext-Chunks gehen an /api/tresor/upload/chunk; nach upload/complete ist die Datei gesichert.
src/uploads.js · Chunk verschlüsseln und übertragen (Auszug)
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:

InformationVerschlüsselt?Wo sichtbar?
Dateiinhalt (PDF, Bild, …)JaNur bei dir nach Entschlüsselung
Master-Schlüssel, Passwort, RecoveryN/ANur kurz im RAM nach Entsperren
Dateiname, OrdnernameJa (im Journal)Verschlüsselt im Änderungsjournal auf dem Server; Klartext nur lokal nach Entsperren
Dateigröße, ZeitstempelNeinLokal; Quota auf dem Server
Tarif, SpeicherbelegungNeinServer (für Abrechnung)
Login-SessionNeinHttpOnly-Cookie deines Browsers

Kryptografische Parameter (Referenz)

ParameterWert
Passwort-AbleitungPBKDF2-SHA256, 600.000 Iterationen
VerschlüsselungAES-256-GCM
IV pro Chunk12 Byte, zufällig (Web Crypto)
AAD-BindungfileUuid + Chunk-Index
Chunk-Größe (Klartext)5 MiB
Recovery-PhraseBIP39, 12 Wörter (128 Bit + Checksumme)
Max. Ordner64 pro Tresor
Session-SchlüsselNicht 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.

EndpointZweck für dich
GET /api/tresor/statusTarif, Quota, Salts, Key-Hüllen
POST /api/tresor/setupErsteinrichtung
GET /api/tresor/recovery-infoRecovery-Hülle zum Entsperren
POST /api/tresor/reset-passwordNeues Passwort nach Recovery
POST /api/tresor/webauthn/*Passkey registrieren / entsperren (optional)
POST /api/tresor/upload/initUpload-Session starten
POST /api/tresor/upload/chunkCiphertext-Chunk senden
POST /api/tresor/upload/completeUpload abschließen
POST /api/tresor/upload/abortUpload abbrechen, Quota freigeben
DELETE /api/tresor/upload/:uploadIdVerschlüsselte Chunks löschen, Quota freigeben

So kannst du es selbst prüfen

Du brauchst kein Spezialwerkzeug, nur die Entwicklertools deines Browsers (F12):

  1. Network: Lade eine Datei hoch. In den Requests solltest du keinen lesbaren PDF- oder Bildinhalt sehen, sondern binäre oder undurchsichtige Payloads.
  2. Sources: Suche im ausgelieferten JavaScript nach PBKDF2_ITERATIONS oder encryptChunk und vergleiche mit den Snippets auf dieser Seite.
  3. Application → IndexedDB → TresorDatabase: Unter fileBlobs siehst du nur verschlüsselte Daten, keine Klartext-Dateien.
  4. 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


Datenputzer Tresor · Öffentliche Client-Dokumentation · Stand Juni 2026