Wie dein Tresor verschlüsselt
Diese Seite beschreibt, was in deinem Browser passiert, bevor Dateien deinen Rechner verlassen. Der Code, der das tut, wird mitsamt Sourcemaps ausgeliefert: in den Entwicklerwerkzeugen unter „Sources" steht crypto.js Zeile für Zeile so da, wie er im Repository liegt. Wie du das prüfst, steht unter Quelltext nachprüfen.
- 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 öffentlicher Ed25519-Schlüssel, mit dem sich ein Passwort-Reset nur prüfen, nicht unterschreiben lässt
Das Prinzip
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 = globalThis.crypto.getRandomValues(new Uint8Array(12));
const ct = await globalThis.crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, kek, masterBits
);
return { wrappedHex: toHex(new Uint8Array(ct)), ivHex: toHex(iv) };
}
// Das ist alles, was beim Setup zum Server geht. Vollstaendig, nicht "unter anderem":
// salt, recoverySalt, recoveryWrapped, recoveryIv,
// recoveryProofKey (oeffentlich), recoveryProofAlg,
// 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 deriveKekFromMnemonic(mnemonic, recoverySalt) {
const enc = new TextEncoder();
const keyMaterial = await globalThis.crypto.subtle.importKey(
"raw", enc.encode(mnemonic.trim()), { name: "PBKDF2" }, false, ["deriveKey"]
);
return await globalThis.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: enc.encode(recoverySalt),
iterations: PBKDF2_ITERATIONS,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false, // extractable: false, Schlüssel bleibt im RAM
["encrypt", "decrypt"]
);
}
// Der Passwort-Kanal nutzt denselben Umschlag, nur mit dem Passwort-Salt.
export async function deriveKekFromPassword(password, salt) {
return deriveKekFromMnemonic(password, salt);
}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 (öffentlicher Ed25519-Schlüssel) | Ja (beim Setup) | Ja (prüft Passwort-Reset-Anfragen, kann selbst nichts unterschreiben und nichts entschlüsseln) |
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 der öffentliche Ed25519-Schlüssel gehen an
POST /api/tresor/setup. Der zugehörige private Schlüssel wird bei Bedarf neu aus deinen 12 Wörtern abgeleitet und nirgends gespeichert.
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 abgeleiteter öffentlicherEd25519-Schlüssel, mit dem er Reset-Anfragen prüfen kann.
export async function generateMnemonic() {
const entropy = globalThis.crypto.getRandomValues(new Uint8Array(16));
const hash = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', entropy));
// 128 Bit Entropie + 4 Bit Pruefsumme aus dem ersten Hash-Byte -> 132 Bit,
// in Bloecken zu 11 Bit auf die 2048 Woerter der BIP39-Liste abgebildet.
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 unterschreibt der Browser eine vom Server ausgegebene Einmal-Aufgabe mit dem Ed25519-Schlüssel aus deinen 12 Wörtern. Unterschrieben wird dabei auch die neue Hülle, eine abgefangene Unterschrift lässt sich also nicht mit anderem Material weiterverwenden.
- 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
Der Schlüssel im Arbeitsspeicher
Verschlüsselung nützt wenig, wenn der Schlüssel danach stundenlang im Arbeitsspeicher liegt. Die entscheidende Frage ist deshalb nicht nur ob entschlüsselt wird, sondern wann und wie lange. Was der Tresor dazu tut:
| Frage | Im Tresor |
|---|---|
| Wann wird der Master-Schlüssel freigelegt? | Erst wenn du dein Passwort oder deine 12 Wörter eingibst. Ohne deine Eingabe passiert nichts, auch nicht beim Öffnen der Seite. |
| Wie lange liegt er im Arbeitsspeicher? | Bis du sperrst, die Auto-Sperre greift oder die Tab-Sperre auslöst. Danach wird er verworfen. |
| Landet er auf der Festplatte? | Nein. Auf dem Server liegen nur die Hüllen (passwordWrapped, recoveryWrapped), lokal nichts. Weder localStorage noch IndexedDB bekommen ihn zu sehen. |
| Lässt sich das an Hardware binden? | Nein. Der Passkey-Weg wurde am 09.09.2026 entfernt (siehe Kasten oben). |
Dass diese Fragen nicht theoretisch sind, zeigte im Mai 2026 ein Fall bei Microsoft Edge: der Browser entschlüsselte gespeicherte Passwörter schon beim Start und hielt sie danach dauerhaft im Arbeitsspeicher, ohne dass jemand danach gefragt hatte.
Wogegen der Tresor schützt und wogegen nicht
Eine Tabelle, in der überall „geschützt" steht, ist keine Sicherheitsdokumentation, sondern Werbung. In zwei Zeilen steht deshalb, dass es keinen technischen Schutz gibt.
| Bedrohung | Schutz im Tresor | Deine Maßnahme |
|---|---|---|
| Server liest Dateiinhalte | Es liegt nur Kryptotext dort, der Schlüssel entsteht bei dir | Ein langes, ungewöhnliches Passwort wählen: gegen die Hülle ist Raten offline möglich |
| Wir liefern manipuliertes JavaScript aus | Technisch keiner. Wer den Code ausliefert, der verschlüsselt, könnte ihn ändern. Das gilt für jeden Anbieter, dessen Krypto im Browser läuft | Quelltext und Prüfsummen vergleichen (nachprüfen). Die Prüfwerte jedes ausgelieferten Bundles stehen offen unter /pruefwerte.json, zu den meisten Ständen liegt zusätzlich eine Kopie im Internet Archive, also außerhalb unserer Reichweite |
| Abhörung im Netz | TLS, und die Blöcke sind schon vorher verschlüsselt | Nichts nötig |
| Schadsoftware auf deinem Rechner | Keiner, solange der Tresor offen ist. Der Schlüssel steht dann im Arbeitsspeicher | Sperren, wenn du fertig bist. Auf einem befallenen Gerät gar nicht erst öffnen |
| Fremder Code im Tresor-Fenster (XSS) | Teilweise. Die CSP erlaubt nur eigene Skripte und keine Inline-Skripte. Der Schlüssel ist extractable: false, lässt sich also nicht auslesen und mitnehmen. Benutzen kann fremder Code ihn trotzdem, solange der Tresor offen ist | Sperren, wenn du fertig bist. Kurze Auto-Sperre einstellen |
| Jemand setzt sich an dein offenes Gerät | Sperre und optionale Tab-Sperre | Kurze Auto-Sperre, Gerät beim Weggehen verriegeln |
| Du verlierst Passwort und die 12 Wörter | Keiner. Wir können den Tresor nicht öffnen | Die 12 Wörter auf Papier, an einem zweiten Ort, und jemandem sagen, dass es sie gibt |
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: gibt es nicht mehr, die Routen wurden entfernt.
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 = globalThis.crypto.getRandomValues(new Uint8Array(12));
const aad = new TextEncoder().encode(`${fileId}:${chunkIndex}`);
const encryptedContent = await globalThis.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, holt der Browser für jeden Block eine kurzlebige, unterschriebene Adresse von unserem Server (GET /api/tresor/upload/:uploadId/chunk/:index) und lädt den Block damit direkt vom Objektspeicher. Unser Server sieht den Kryptotext dabei nicht, er stellt nur die Adresse aus. Entschlüsselt wird anschließend lokal mit deinem Sitzungsschlüssel. Für große Dateien läuft das im Strom: Klartextblöcke entstehen nacheinander, nicht alle gleichzeitig im Arbeitsspeicher.
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 -> naechster Chunk.
// Kein Array, das die ganze Datei im Klartext sammelt.
}
});
// new Blob([stream]) waere falsch: ein ReadableStream ist kein gueltiger Blob-Teil
// und wuerde zu "[object ReadableStream]" verstringlicht, also 23 Byte Schrott.
// Response liest den Strom korrekt aus.
const streamed = await new Response(stream).blob();
return originalType ? new Blob([streamed], { type: originalType }) : streamed;
}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' // journalLastEventId, journalDeviceTag,
// journalDeviceSeq, journalBurnedSeq
});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/foldersWarum Verschlüsseln allein hier nicht reicht
Jeder Eintrag ist für sich verschlüsselt und mit einem Prüfwert versehen, aber das bindet nur seinen Inhalt, nicht seine Position. Ein Server könnte einzelne Einträge weglassen oder umsortieren: eine Datei fehlte dann still in der Liste, oder eine alte Umbenennung gewänne gegen eine neue. Lesen könnte er trotzdem nichts, aber ein Safe, dessen Inhaltsverzeichnis sich von außen kürzen lässt, ist kein guter Safe.
Deshalb trägt jeder Eintrag im verschlüsselten Teil eine Gerätekennung und einen je Gerät fortlaufenden Zähler. Beides steht damit unter demselben Prüfwert wie der Inhalt und lässt sich nicht fälschen. Beim Abgleich müssen die Zähler eines Geräts eine lückenlose Reihe ab 1 ergeben. Fehlt eine Zahl, sagt der Tresor das.
- Je Gerät, nicht global: ein globaler Zähler würde kollidieren, wenn zwei deiner Geräte gleichzeitig schreiben. Ein echter Konflikt sähe dann aus wie ein Angriff.
- Der Tresor sperrt bei einem Fund nicht. Deine Dateien sind unversehrt, nur die Liste ist womöglich unvollständig. Wer an dieser Stelle ausgesperrt würde, käme an gar nichts mehr heran, obwohl alles noch da ist.
- Was das nicht erkennt: was ein Gerät nie gesehen hat, kann es nicht vermissen. Unterschlägt der Server die jüngsten Einträge eines anderen Geräts, fällt das hier nicht auf. Die eigenen fehlenden Einträge erkennt jedes Gerät.
- Einträge aus der Zeit vor dieser Prüfung tragen keinen Zähler und werden davon ausgenommen, sonst schlüge jeder bestehende Tresor sofort Alarm.
Upload
- 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
- Für jedes Stück holt der Browser eine kurzlebige, unterschriebene Adresse (
POST /api/tresor/upload/sign) und lädt den Kryptotext perPUTdirekt zum Objektspeicher. Unser Server bekommt die Bytes nie zu sehen, er stellt nur die Adresse aus und zählt den belegten Platz. NachPOST /api/tresor/upload/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);
// Kurzlebige unterschriebene Adresse von unserem Server holen ...
const { url } = await (await authenticatedFetch(`${API_URL}/upload/sign`, {
method: 'POST', body: JSON.stringify({ uploadId, chunkIndex: c })
})).json();
// ... und den Kryptotext direkt zum Objektspeicher schieben.
// Bewusst ohne Authorization-Kopf: die Adresse IST der Ausweis.
await fetch(url, { method: 'PUT', body: chunkBody });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, Chunk-Anzahl, Zeitstempel | Nein | Server (pro Datei, unverschlüsselt) |
| Tarif, Speicherbelegung | Nein | Server (für Abrechnung) |
| Login-Session | Nein | HttpOnly-Cookie deines Browsers |
| Verschlüsselte Blöcke selbst | Ja | Objektspeicher bei Hetzner in Nürnberg. Der Anbieter sieht Größe, Anzahl und Zugriffszeiten der Blöcke, aber keinen Inhalt und keine Dateinamen |
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 + 4 Bit Prüfsumme) |
| Reset-Nachweis | Ed25519, Server hält nur den öffentlichen Schlüssel |
| Einmal-Aufgabe beim Reset | 32 Byte zufällig, 5 Minuten gültig, einmalig verwendbar |
| 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.
- 600.000 Iterationen sind der aktuelle OWASP-Wert für PBKDF2-HMAC-SHA256, das Verfahren selbst ist in NIST SP 800-132 beschrieben.
- Was das nicht leistet: Argon2id erschwert Angriffe zusätzlich über den Speicherbedarf, PBKDF2 nur über Rechenzeit. Spezialhardware rechnet PBKDF2 daher günstiger durch. Ein langes, ungewöhnliches Passwort gleicht das aus, ein kurzes oder geläufiges nicht.
Server (Überblick)
Hinter /api/tresor/* läuft ein separater Dienst. Er prüft deine Datenputzer-Session, verwaltet Tarif und belegten Platz und stellt kurzlebige unterschriebene Adressen aus. Die verschlüsselten Blöcke selbst laufen nicht über ihn, sondern direkt zwischen deinem Browser und dem Objektspeicher.
| Endpoint | Zweck für dich |
|---|---|
GET /api/tresor/status | Tarif, belegter Platz, Salz. Ohne die Passwort-Hülle: die gibt es nur über den Endpunkt darunter |
POST /api/tresor/setup | Ersteinrichtung |
GET /api/tresor/recovery-info | Recovery-Hülle zum Entsperren |
POST /api/tresor/reset-password | Neues Passwort nach Recovery |
GET /api/tresor/unlock-material | Salz und Passwort-Hülle, nur beim Entsperren abgerufen und streng ratenbegrenzt |
POST /api/tresor/upload/init | Upload-Session starten |
POST /api/tresor/upload/sign | Kurzlebige unterschriebene PUT-Adresse für einen Block. Die Bytes gehen danach am Server vorbei direkt zum Objektspeicher |
GET /api/tresor/upload/:uploadId/chunk/:index | Dasselbe für das Herunterladen: unterschriebene GET-Adresse, nicht der Block selbst |
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
Diese Seite beschreibt, wie der Tresor gebaut ist. Ob dein Browser wirklich genau diesen Code ausführt, ist eine eigene Frage, und dafür gibt es eine eigene Seite mit einer Schritt-für-Schritt-Anleitung: Quelltext nachprüfen.
Dort steht unter anderem:
- Der Krypto-Worker in voller Länge. Die Datei ist gut ein Kilobyte groß. Du kannst sie ganz lesen und selbst nachsehen, dass darin keine einzige Anweisung steht, die etwas ins Netz schicken könnte, und kein einziges Wort über Passwörter.
- Haltepunkt im lesbaren Original. Wir liefern Sourcemaps mit, deshalb zeigen die Entwicklertools den echten Quelltext samt Kommentaren statt des gestauchten Codes. Du kannst mitten in der Verschlüsselung anhalten und die Werte ansehen.
- Der Gegenbeweis. Über die Überschreibungen der Entwicklertools kannst du den Krypto-Code bei dir lokal kaputt machen. Geht der Upload danach nicht mehr, war es der echte Code.
- Application → IndexedDB → TresorDatabase: Unter
fileBlobssiehst du nur verschlüsselte Daten, keine Klartext-Dateien.
Die Prüfsumme der Dateien, die dein Browser gerade geladen hat, rechnet er dort selbst aus. Verglichen wird sie gegen unser offenes Logbuch unter /pruefwerte.json, in dem jeder ausgelieferte Stand steht, jeder Eintrag mit dem vorherigen verkettet. Zu den meisten Ständen liegt zusätzlich eine Kopie im Internet Archive. Wir könnten das Logbuch also nicht rückwirkend umschreiben, ohne dass die Kette bricht.
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 Speicherbelegung, Upload-IDs, Chunk-Anzahlen, Dateigrößen und Zeitstempel (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. Wir können sie nicht zurückholen, weil wir sie nie lesen konnten. Ein Anbieter, der dein Passwort zurücksetzen kann, kann auch deine Dokumente lesen; beides zusammen geht nicht.
- Fremder Code auf der Seite: Die Content-Security-Policy erlaubt nur Skripte von dieser Domain und keine Inline-Skripte. Im Krypto-Pfad steckt außerdem keine einzige fremde Bibliothek:
crypto.jsnutzt nur die eingebaute Web-Crypto des Browsers und die BIP39-Wortliste. Was es nicht gibt, kann auch nicht über eine Lieferkette vergiftet werden. Bliebe der Fall, dass wir manipulierten Code ausliefern, und genau dafür gibt es Quelltext nachprüfen. - 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.
- Verfügbarkeit ist kein Krypto-Versprechen: Die Verschlüsselung schützt deine Inhalte, nicht ihren Fortbestand. Gegen einen Ausfall auf unserer Seite hilft nur eine eigene Kopie. Lade dir das, worauf es dir wirklich ankommt, zusätzlich herunter.
Hinweise oder Anregungen: support@datenputzer.de