barox transfer · Verschlüsselungs-Architektur
Zero-Knowledge / Ende-zu-Ende · libsodium

Wie die Daten verschlüsselt werden

barox transfer ist Zero-Knowledge: Die gesamte Ver- und Entschlüsselung läuft im Browser (libsodium / WebAssembly). Der Server speichert ausschließlich Chiffrate und für die jeweiligen Empfänger gewickelte Schlüssel – niemals Klartext, Dateinamen, Passphrasen oder entschlüsselte Schlüssel. Selbst bei vollständiger Server-Kompromittierung bleibt der Inhalt unlesbar.

1 · Eingesetzte Krypto-Primitive

Alle Verfahren stammen aus libsodium. Es wird nichts selbst „erfunden“ – nur etablierte, autorisierte Konstruktionen kombiniert.

SchichtVerfahren (libsodium)AlgorithmusZweck
Identität crypto_box_keypair X25519 Persönliches Schlüsselpaar pro internem Nutzer
Passwort→Schlüssel crypto_pwhash Argon2id (MODERATE) Leitet aus Passphrase / Passwort einen Schlüssel ab (KDF)
Key-Wrapping crypto_secretbox XSalsa20-Poly1305 Verschlüsselt den privaten Schlüssel bzw. File-Key symmetrisch
Empfänger-Wrap (intern) crypto_box_seal X25519 + XSalsa20-Poly1305 Versiegelt den File-Key anonym an einen Public Key
Datei-Streaming crypto_secretstream XChaCha20-Poly1305 Chunkweise authentifizierte Verschlüsselung großer Dateien
Metadaten crypto_secretbox XSalsa20-Poly1305 Dateinamen, Größen & Nachricht (mit dem File-Key)
Zufall randombytes_buf CSPRNG Salts, Nonces, File-Keys, Passwörter, Recovery-Codes

Quelle: public/assets/js/crypto.js

2 · Schlüssel-Hierarchie

Jeder Transfer bekommt einen frischen, symmetrischen File-Key (32 Byte, rein zufällig). Dieser eine Schlüssel verschlüsselt sowohl die Dateien als auch die Metadaten und wird dann für jeden Empfänger einzeln gewickelt:

File-Key (32 B, zufällig, pro Transfer) │ ├─ verschlüsselt die Datei-Chunks → crypto_secretstream (XChaCha20-Poly1305) ├─ verschlüsselt die Metadaten → crypto_secretbox (Namen, Größen, Nachricht) │ └─ wird pro Empfänger gewickelt: ├─ intern → crypto_box_seal( File-Key , Public-Key des Empfängers ) └─ extern → secretbox( File-Key , Argon2id(Passwort) ) Identitäts-Schlüsselpaar (X25519, pro Nutzer) ├─ Public-Key → liegt im Klartext auf dem Server (für Versiegelung an den Nutzer) └─ Private-Key → NIE im Klartext auf dem Server; doppelt gewickelt: ├─ secretbox( Private-Key , Argon2id(Passphrase) ) └─ secretbox( Private-Key , Argon2id(Recovery-Code) )
Kernidee: Der File-Key existiert im Klartext nur kurz im Browser. Auf dem Server liegt er ausschließlich gewickelt – entweder hinter dem privaten Schlüssel des internen Empfängers oder hinter dem out-of-band geteilten Passwort des externen Empfängers.

3 · Schlüssel-Setup eines internen Nutzers Client

Microsoft 365 / Azure AD authentifiziert den Nutzer, liefert aber kein Schlüsselmaterial für die Verschlüsselung. Deshalb legt jeder Nutzer einmalig eine separate Passphrase fest, aus der lokal sein Schlüsselpaar abgeleitet/geschützt wird.

Browser Server 1. Schlüsselpaar erzeugen (X25519) 2. Recovery-Code würfeln (5×4 Zeichen) 3. Private-Key 2× wickeln: encP = secretbox(priv, Argon2id(Passphrase, saltP)) encR = secretbox(priv, Argon2id(Recovery, saltR)) 4. senden: public_key, encP, saltP, encR, saltR ──────────────▶ speichert nur diese Werte (Private-Key bleibt im Browser) 5. Recovery-Code dem Nutzer anzeigen (nur dieses eine Mal)

Quelle: public/assets/js/keys-setup.js, crypto.js → wrapSecret()

4 · Senden eines Transfers Client

Browser (Absender) Server 1. File-Key = secretstream_keygen() 2. Metadaten {Namen, Größen, Nachricht} → encMetadata = secretbox(meta, File-Key) 3. pro Empfänger-E-Mail: lookup(email) ──────────────▶ Public-Key bekannt? ◀────────────── ja → public_key │ nein → not found ┌─ intern: wrapped = box_seal(File-Key, public_key) └─ extern: pw = randomPassword(18) wrapped = secretbox(File-Key, Argon2id(pw, salt)) 4. Transfer anlegen (encMetadata, Empfängerliste mit wrapped_key) ────────────▶ speichert Chiffrat-Hülle + wrapped keys 5. Dateien chunkweise verschlüsseln: header + secretstream(chunk) ──────────────▶ legt Chunks ab (Bytes, opak) 6. complete ────────────▶ versendet Benachrichtigungs-E-Mails (intern: Link · extern: nur Link) 7. externes Passwort dem Absender anzeigen → out-of-band an Empfänger

Interner Empfänger

File-Key wird anonym an den Public Key versiegelt (crypto_box_seal). Nur der zugehörige private Schlüssel – also der Empfänger mit seiner Passphrase – kann ihn öffnen.

Externer Empfänger

Die App würfelt ein starkes Zufallspasswort. Der File-Key wird mit Argon2id(Passwort) verschlüsselt. Das Passwort erreicht den Server nie – der Absender teilt es out-of-band (Telefon/SMS) mit.

Quelle: public/assets/js/send.js

5 · Datei-Streaming & großer Dateien

Dateien werden nicht am Stück, sondern gechunkt verschlüsselt (crypto_secretstream_xchacha20poly1305). Das hält den Speicher­bedarf niedrig, erlaubt resumierbaren Upload/Download und authentifiziert jeden Chunk einzeln – inklusive Reihenfolge und korrektem Stream-Ende.

Datei → Klartext-Chunks à CHUNK_BYTES (Default 4 MB) Chunk 0: [ header (24 B) ][ secretstream(chunk0, TAG_MESSAGE) ] Chunk 1: [ secretstream(chunk1, TAG_MESSAGE) ] … Chunk n: [ secretstream(chunkN, TAG_FINAL) ] ← markiert das Ende Pro Chunk +17 B Auth-Tag (ABYTES). Empfänger liest exakte Frames (CHUNK_BYTES + 17) und entschlüsselt streamend auf die Platte.
Server-Limit: CHUNK_BYTES (Default 4 MB) muss kleiner als PHP post_max_size bleiben. Die Klartext-Chunkgröße steckt verschlüsselt in den Metadaten, damit der Empfänger die Frame-Grenzen kennt.

Quelle: crypto.js → createEncryptor/createDecryptor, send.js → uploadAll, receive-core.js

6 · Empfangen & Entschlüsseln Client

Intern (angemeldeter Nutzer)

1. Passphrase eingeben 2. priv = secretbox_open(encP, Argon2id(Passphrase, saltP)) // Private-Key entsperren 3. File-Key = box_seal_open(wrapped_key, pub, priv) 4. Metadaten entschlüsseln, dann Datei-Frames streamend entschlüsseln

Extern (Link + Passwort)

1. Link öffnen, out-of-band erhaltenes Passwort eingeben 2. File-Key = secretbox_open(wrapped_key, Argon2id(Passwort, salt)) 3. Metadaten + Datei-Frames entschlüsseln (identisch zum internen Pfad)

Quelle: receive-internal.js, receive-external.js, receive-core.js

7 · Eingehender Kunden-Upload (Einladung)

Ein Mitarbeiter lädt einen Kunden über einen tokengesicherten Link (/u/{token}) zum Hochladen ein. Der Kunde braucht kein Konto. Seine Dateien werden im Browser verschlüsselt und an den öffentlichen Schlüssel des Mitarbeiters versiegelt – nur dieser kann sie später mit seiner Passphrase öffnen.

Browser (Kunde, ohne Konto) Server 1. Einladung öffnen ──────────────▶ liefert Public-Key des Mitarbeiters 2. File-Key erzeugen, Dateien verschlüsseln 3. wrapped = box_seal(File-Key, pub Mitarbeiter) 4. Upload (Chunks + wrapped) ──────────────▶ erscheint als Transfer unter /t/{uuid}

Quelle: upload-invite.js, InvitationUploadController.php

8 · Wiederherstellung (Recovery)

Beim Setup erhält der Nutzer einen einmaligen Recovery-Code, der denselben privaten Schlüssel separat wickelt. Vergisst er seine Passphrase, entsperrt er den privaten Schlüssel mit dem Code und setzt eine neue Passphrase – ohne dass das Zero-Knowledge-Prinzip bricht.

1. priv = secretbox_open(encR, Argon2id(Recovery-Code, saltR)) 2. neu wickeln: encP' = secretbox(priv, Argon2id(neue Passphrase, saltP')) 3. rotate-passphrase ──────────▶ ersetzt nur encP + saltP (Public-Key bleibt gleich)
Kein Hintertürchen: Gehen Passphrase und Recovery-Code verloren, sind die empfangenen Daten unwiederbringlich verloren. Das ist eine bewusste Eigenschaft von Zero-Knowledge – der Server kann nicht aushelfen.

Quelle: keys-setup.js → bindRecover

9 · Was der Server speichert – und was nie

✅ Auf dem Server (alles opak)

  • Datei-Chiffrate (außerhalb des Webroots)
  • Gewickelte File-Keys pro Empfänger
  • Verschlüsselte Metadaten (Namen, Nachricht)
  • Public Keys der Nutzer
  • Gewickelter Private-Key (encP/encR + Salts)
  • Empfänger-E-Mails, Größen, Zeitstempel, Audit-Log

⛔ Niemals auf dem Server

  • Datei-Klartext oder Klartext-Dateinamen
  • File-Keys im Klartext
  • Passphrasen, Recovery-Codes
  • Externe Passwörter (out-of-band)
  • Entschlüsselte private Schlüssel

Auslieferung der Chiffrate erfolgt nur über PHP mit Zugriffsprüfung, Ablauf- und Download-Limits; zusätzlich HTTPS/HSTS, strenge CSP (script-src 'self'), CSRF-Schutz und Rate-Limiting der externen Endpunkte.

10 · Restrisiken & bewusste Entscheidungen