Virtuelles Dateisystem

Cryptomator stellt ein virtuelles Laufwerk bereit. Dateien lassen sich genauso hinzufügen, bearbeiten und entfernen, wie Sie es bei einem USB-Stick gewöhnt sind.

Die Daten werden mit sogenannter “transparenter” Verschlüsselung ver- und entschlüsselt. Das bedeutet, dass es keine unverschlüsselte Version einer Datei auf Ihrer Festplatte gibt. Immer wenn Sie auf Ihre Dateien zugreifen, wird Cryptomator darüber informiert und wird in diesem Moment die Daten ver- bzw. entschlüsseln.

Zurzeit verwendet Cryptomator das WebDAV-Protokoll, da dieses durch die Betriebssysteme Windows, Mac und Linux unterstützt wird. WebDAV basiert auf dem HTTP-Protokoll und wird genutzt, um Cryptomator als Server agieren zu lassen, der nur sogenannte Loopback-Verbindungen mit dem eigenen Rechner zulässt. Sobald das Dateisystem durch dieses Protokoll auf Dateien zugreifen will, bearbeitet Cryptomator diese Anfrage durch die im Folgenden beschriebenen Schichten.

Ableitung des Hauptschlüssels

Jeder Tresor hat seinen eigenen 256-Bit-Verschlüsselungs- sowie einen MAC-Hauptschlüssel, die für die Verschlüsselung von dateispezifischen Schlüsseln beziehungsweise für die Dateiauthentifizierung benutzt werden.

Diese Schlüssel sind zufällige Sequenzen, die durch einen kryptographisch sicheren Zufallszahlengenerator (CSPRNG) generiert werden. Wir nutzen SecureRandom mit SHA1PRNG, initialisiert mit einem Seed von 440 Bits aus SecureRandom.getStrongInstance().

Aus dem Passwort des Tresors wird durch Verwendung von scrypt ein KEK abgeleitet wird. Beide Hauptschlüssel werden durch Key Wrapping mit diesem KEK verschlüsselt.

encryptionMasterKey := createRandomBytes(32)
macMasterKey := createRandomBytes(32)
kek := scrypt(password, scryptSalt, scryptCostParam, scryptBlockSize)
wrappedEncryptionMasterKey := aesKeyWrap(encryptionMasterKey, kek)
wrappedMacMasterKey := aesKeyWrap(macMasterKey, kek)

Ableitung des KEK

Die umhüllten Schlüssel sowie die Parameter, die zum Erlangen des KEK nötig sind, werden dann in einer JSON-Datei namens masterkey.cryptomator gespeichert. Diese befindet sich im Wurzelverzeichnis des Tresors und sieht in etwa so aus:

{
  "version": 5, /* vault version for checking software compatibility */
  "scryptSalt": "QGk...jY=",
  "scryptCostParam": 16384,
  "scryptBlockSize": 8,
  "primaryMasterKey": "QDi...Q==", /* wrappedEncryptionMasterKey */
  "hmacMasterKey": "L83...Q==", /* wrappedMacMasterKey */
  "versionMac": "3/U...9Q=" /* HMAC-256 der Tresor-Version um unerkannte Downgrade-Angriffe zu verhindern */
}

Beim Entsperren des Tresors wird der KEK, der aus dem Passwort und den Parametern in der Datei masterkey.cryptomator ermittelt wird, benutzt, um die gespeicherten Hauptschlüssel zu enthüllen (d.h. zu entschlüsseln). Erst mit den enthüllten Hauptschlüsseln ist die Entschlüsselung der Daten möglich.

Entschlüsselung des Hauptschlüssels

Verschlüsselung des Dateinamens

Neben den Dateiinhalten werden auch die Dateinamen verschlüsselt.

Cryptomator nutzt den Modus AES-SIV, um Dateien und Verzeichnisnamen zu verschlüsseln. Zusätzlich wird eine eindeutige Verzeichnis-ID des übergeordneten Verzeichnisses als Zusatzdaten übergeben. Dies verhindert das unerkannte Verschieben von verschlüsselten Dateien in andere Verzeichnisse.

ciphertextName := base32(aesSiv(cleartextName, parentDirId, encryptionMasterKey, macMasterKey))

Bei Dateien ist das Ergebnis der verschlüsselte Name.

Handelt es sich hingegen um ein Verzeichnis, wird eine Null vorangestellt. Dann wird eine Datei dieses Namens erstellt, in die wir eine eindeutige Kennzeichnung (genauer eine UUID) schreiben. Das dazugehörige Verzeichnis wird dann an folgendem Ort gespeichert:

dirId := createUuid()
dirIdHash := base32(sha1(aesSiv(dirId, null, encryptionMasterKey, macMasterKey)))
dirPath := vaultRoot + '/d/' + substr(dirIdHash, 0, 2) + '/' + substr(dirIdHash, 2, 30)

Indem alle Verzeichnisse nebeneinander liegen, wird nicht nur die Verzeichnishierarchie verschleiert, sondern es wird auch die Pfadtiefe begrenzt, um die Kompatibilität mit einigen Cloud-Diensten sicherzustellen.

Verschlüsselung des Dateinamen

* Eindeutige Kennzeichnung wird für jedes Verzeichnis erstellt

Verschlüsselung des Dateikopfs

Der Dateikopf enthält Metadaten, die für die Verschlüsselung des Dateiinhalts benötigt werden. Er besteht aus 88 Bytes.

  • 16 Bytes mit nonce, die für die Verschlüsselung der Kopfdaten verwendet wird
  • 40 Bytes mit AES-CTR verschlüsselte Kopfdaten, bestehend aus:
    • 8 Bytes gefüllt mit 1 für den zukünftigen Gebrauch (früher benutzt für die Dateigröße)
    • 32 Bytes mit dem Dateiinhalteschlüssel
  • 32 Bytes mit dem MAC der vorhergehenden 56 Bytes

Die Dateigröße wird im Dateikopf gespeichert, da die echte Dateigröße verschleiert wird. Dies wird im nächsten Abschnitt beschrieben. Zudem wird jede Datei jeweils mit einem eigenen Dateischlüssel verschlüsselt. Dieser wird ebenfalls im Dateikopf abgelegt.

headerNonce := createRandomBytes(16)
contentKey := createRandomBytes(32)
cleartextPayload := 0xFFFFFFFFFFFFFFFF . contentKey
ciphertextPayload := aesCtr(cleartextPayload, encryptionMasterKey, headerNonce)
mac := hmacSha256(headerNonce . ciphertextPayload, macMasterKey)

Verschlüsselung des Dateikopfes

* Zufällig bei jeder Dateiänderung

Verschlüsselung des Dateiinhalts

Hier wird der aktuelle Dateiinhalt verschlüsselt.

Der unverschlüsselte Dateiinhalt wird in mehrere Stücke zerteilt, jedes bestehend aus 32 KiB. Diese Stücke werden dann jeweils verschlüsselt und folgendermaßen um weitere 48 Bytes ergänzt:

  • 16 Bytes mit dem nonce
  • bis zu 32 KiB mit AES-CTR und dem Dateiinhalteschlüssel verschlüsselte Daten
  • 32 Bytes mit dem MAC über
    • die nonce aus dem Dateikopf (um zu verhindern, dass das Stück unerkannt mit Stücken in anderen Dateien vertauscht wird),
    • die laufende Nummer des Stücks (um zu verhindern, dass Stücke unerkannt innerhalb einer Datei vertauscht werden können),
    • die nonce des Stücks und
    • die verschlüsselten Daten

Nach der Verschlüsselung werden diese Stücke wieder in derselben Reihenfolge zusammengefügt. Dabei kann das letzte Stück weniger als 32 KiB Daten enthalten, wenn die Datei nicht eine Länge hat, die ein Vielfaches von 32 KiB ist.

cleartextChunks[] := split(cleartext, 32KiB)
for (int i = 0; i < length(cleartextChunks); i++) {
  chunkNonce := createRandomBytes(16)
  ciphertextPayload := aesCtr(cleartextChunks[i], contentKey, chunkNonce)
  mac := hmacSha256(headerNonce . bigEndian(i) . chunkNonce . ciphertextPayload, macMasterKey)
  ciphertextChunks[i] := chunkNonce . ciphertextPayload . mac
}
ciphertextFileContent := join(ciphertextChunks[])

Verschlüsselung des Dateiinhalts

* Zufällig bei jeder Stückänderung

Kürzung der Pfade

Dieser Schritt ändert die Dateiinhalte nicht. Der einzige Zweck ist es, die Kompatibilität mit einigen Microsoft-Produkten sicherzustellen, die noch immer keine langen Pfade unterstützen.

Trotz der flachen Ordnerhierarchien, die durch die Dateinamensverschlüsselung erzeugt wurde, kann der Pfad zu einer Datei länger als 255 Zeichen sein. Immer wenn ein Dateiname einen Grenzwert überschreitet, wird dieser durch einen wesentlich kürzeren SHA-1-Hash ersetzt und bekommt eine .lng-Erweiterung. Zusätzlich wird eine Datei gleichen Namens im Verzeichnis m erstellt, die die Zuordnung des ursprünglichen Namens erlaubt.

Durch diesen Schritt wird keine zusätzliche Sicherheit erreicht, sondern lediglich die Kompatibilität maximiert.

Zielverzeichnis

Nachdem die Dateien gemäß aller beschriebenen Schritte verarbeitet wurden, werden sie im Zielverzeichnis des Tresors abgelegt.

Hiermit endet die Arbeit von Cryptomator und das Synchronisationsprogramm des Cloud-Anbieters kann mit der Synchronisation der verschlüsselten Daten beginnen.

This website uses cookies for statistical purposes. For more infos please see our privacy policy.