Zum Inhalt springen

Dateiformat der Archive

Dieses Kapitel beschreibt das interne Speicherformat der Vanderplanki Desktop-Archive.

Eine Beispielimplementierung, die Vanderplanki Desktop-Archive praktisch ohne Vanderplanki Desktop lesen kann, ist der Vanderplanki Reader.

Auf der obersten Ebene arbeitet Vanderplanki Desktop mit Item Lists. Diese werden gemeinsam mit Thumbnails im CAS für Metaobjekte gespeichert. Dateinhalte werden in einem gesonderten CAS für Objekte gespeichert.

Item Lists sind JSON-codierte Objekte, die ein Array mit Items beinhalten. Items können dabei zum Beispiel Ordner, Dateien oder E-Mails sein. Der genaue Aufbau von Item Lists und Items wird weiter unten beschrieben.

Ein CAS speichert beliebig viele Objekte anhand der SHA-256-Hashwerte ihrer Inhalte. Jedes Vanderplanki Desktop-Archiv hat sowohl ein CAS für Objekte als auch eines für Metaobjekte. Objekte innerhalb eines CAS können eine beliebige Größe haben.

Die Objekte, die in einem der beiden CAS gespeichert sind, werden nicht unmittelbar in den Dateisystemen der Archivspeicher abgelegt, sondern in Form von Chunks. Kleine Objekte werden in einem einzelnen Chunk gespeichert. Große Objekte werden in mehrere Chunks aufgeteilt und es wird ein zusätzlicher Chunk angelegt, welcher eine Liste der einzelnen Fragment-Chunks beinhaltet. Dies wird detaillierter unter Content Addressable Storage beschrieben.

Chunks werden von Vanderplanki Desktop ggf. komprimiert, um Speicherplatz zu sparen.

Auch Chunks werden nicht unmittelbar in den Dateisystemen der Archivspeicher abgelegt, sondern zu Chunk Packs zusammengefasst. Die maximale Größe der Chunk Packs hängt vom Typ des CAS ab.

Damit Vanderplanki Desktop weiß, welcher Chunk sich an welcher Stelle welches Chunk Packs befindet, gibt es eine Chunk Map, welche aus einer oder mehreren Chunk Map Segments besteht.

Chunk Packs und Chunk Map Segments bilden zusammen den Chunk Storage. Dieser wird detaillierter unten auf dieser Seite beschrieben.

Eine Archive Version ist sozusagen der Einstiegspunkt. Die Archive Version beinhaltet unter anderem für das Objekte-CAS als auch für das Metaobjekte-CAS jeweils eine Liste aller zugehörigen Chunk Packs und Chunk Map Segments, sowie der SHA-256-Hash der Stamm-Item List der jeweiligen Archivversion. Der Aufbau der Archive Version ist weiter unten beschrieben.

Sowohl die Archive Version als auch die zugehörigen Chunk Packs und Chunk Map Segments werden als Blob-Dateien in den Archivspeichern gespeichert. Diese tragen als Namen den SHA-256-Hashwert ihrer Inhalte. Dies erleichtert eine Prüfung auf mögliche Fehler erheblich.

In einem Archivspeicher werden abgelegt:

  • Die Blob-Dateien der einzelnen Archive, die auf dem Archivspeicher gespeichert sind
  • Verschlüsselte Datenbanksicherungen
  • Eine Datei store.txt, die im Wesentlichen die UUID des Archivspeichers beinhaltet. Dies erleichtert die Identifikation des Archivspeichers.
  • Directoryblobs
    • Directoryef094cbe-6169-4acd-ad35-c171dcc343d3
      • Directory2b
        • 2b958309de842686b53db4fdbe5bd388ac426e23589defcf18172e31c9d57b98
      • Directorya2
        • a228effb2c209f473e1ff245c90a30cf6d8e29617c37734ae484e7d900f46e71
        • a237b2a474fa47f687d86ed377e5c4d278dd5a973425a39a4b15bf476e9193cd
        • a29f31a2333af5bad9dcbab03b22b6bb13eb3d2bfcc91cfbeffa1ce8b6c83127
      • Directoryc3
        • c34d8ff3e5361afa32a8763e4c57c6749f6635e2400dca289f7c37d34ac0724b
        • c3a326e7826df18994e3cb44b8838b526bb3a0cee42d5508f11296018531caa2
  • Directorydb-backups
    • Directory2024
      • Directory2024-03
        • encrypted-db-backup-2024-03-10-085256101.txt
        • encrypted-db-backup-2024-03-10-090412106.txt
  • store.txt

Schlüssel ermitteln, die zur Entschlüsselung der Archivversion und der CAS-Inhalte benötigt werden

Abschnitt betitelt „Schlüssel ermitteln, die zur Entschlüsselung der Archivversion und der CAS-Inhalte benötigt werden“

Sowohl Datenbanken als auch Datenbanksicherungen sind JavaScript-Dateien, welche eine Sektion beinhalten, die die Datenbank im JSON-Format enthalten. Die eigentlichen JSON-Daten sind umschlossen von Zeilen mit dem folgenden Inhalt:

// ===== BEGIN JSON =====

und

// ===== END JSON =====

Die eigentlichen JSON-Daten sind umschlossen von einem JSON-Umschlag (“JSON Envelope”). Dessen Nutzdaten sind entweder im Klartext verfügbar (body) oder verschlüsselt. Im letzteren Fall befinden sich die verschlüsselten Nutzdaten BASE64-codiert in der Eigenschaft encryptedBody.

Sind die Nutzdaten verschlüsselt, ist der zur Entschlüsselung zu verwendende Schlüssel zunächst über die Schlüsselableitungsfunktion Scrypt abzuleiten. Die hierfür erforderlichen Parameter salt, keyLength, cost, blockSize und parallelization sind genauso wie der verschlüsselte Schlüssel encryptionKey in keyProtectors -> password bzw. recoveryKey zu finden.

Innerhalb der Nutzdaten befindet sich eine Eigenschaft archives, welche ein JSON-Array beinhaltet, das in der Datenbank verzeichneten Archive auflistet. Die wichtigen Eigenschaften sind hier archiveId, welche die ID des Archivs und damit auch den Namen des Unterverzeichnisses der Archivspeicher angibt, der Name des Archivs name, der Archivtyp type (fileArchive oder mailArchive) sowie archiveVersionKey und casKey. Letztere beinhalten die Schlüssel, die zur Entschlüsselung der Archivspeicher erforderlich sind.

Im Falle von archiveVersionKey ist dies immer ein hexadezimal codierter String. Der casKey ist ein JSON-Umschlag wie oben beschrieben. Hier ist der Schlüssel verschlüsselt, wenn das Archiv mit einem zusätzlichen Kennwort geschützt wurde. (Der Schutz von Archiven mit einem zusätzlichen Kennwort ist ein Feature, das in der aktuellen Version von Vanderplanki Desktop noch nicht generell verfügbar ist.)

Die gewünschte Archivversion kann über das Array versions ermittelt werden (archiveVersionHash). Die Archivversion mit dem höchsten Zeitstempel (created) ist die aktuelle. Beinhaltet der zu entschlüsselnde Archivspeicher nicht die aktuelle Archivversion, kann diese über das Array replicas ermittelt werden.

0 - false (byte) 1 - true (byte)

One byte.

Little endian-encoded signed 32-bit integer (4 bytes).

Little endian-encoded signed 64-bit integer (8 bytes).

16 bytes.

SHA256 hash (32 bytes)

  • Hash (see above)
  • Length (int32)
  • Count (int32)
  • Item 1 .. Item n
  • Auth key (16 bytes)
  • Cipher key (16 byte)
  • Initialization vector (IV) (16 bytes)
  • Encrypted data (AES-256-CBC)
  • Message authentication code (HMAC-SHA-256) of IV and encrypted data (32 bytes)

Data is compressed using GZIP.

Signed 32-bit integers are written as a JSON number.

Signed 64-bit integers are written as a JSON string because they can potentially exceed JavaScript’s Number.MAX_SAFE_INTEGER (which is 9007199254740991) on the positive side and Number.MIN_SAFE_INTEGER (which is -9007199254740991) on the negative side.

SHA256 hashes (32 bytes) are written as hexadecimal-encoded strings (0-9, a-f).

Instants (timestamps) are written as a JSON string in simplified extended ISO format (ISO 8601), which is always 24 characters long (YYYY-MM-DDTHH:mm:ss.sssZ). The timezone is always zero UTC offset (suffix “Z”).

The archive version contents are encrypted with AES-256 as a whole, using the archive version key of the archive.

  • 1d 05 00 - Signature
  • 01 or 02 - Version
  • ID of the archive (UUID)
  • Creation time of this archive version (Instant)
  • Parent archive version hash (Hash). If there is no parent archive version, a hash only consisting of zeros is written.
  • Root meta object hash (Hash)
  • Meta objects - Map segment keys (UntypedBlobKey[]), sorted by hash
  • Meta objects - Pack keys (UntypedBlobKey[]), sorted by hash
  • Objects - Map segment keys (UntypedBlobKey[]), sorted by hash
  • Objects - Pack keys (UntypedBlobKey[]), sorted by hash
  • List of index files (Version >= 2 only): Array of
    • Byte length of the name of the index file (byte, 0..127)
    • UTF-8-encoded name of the index file
    • Index file key (UntypedBlobKey)

The map consists of one or more map segments, whose contents are encrypted with AES-256 as a whole, using the CAS key of the archive.

  • 1d 05 01 - Signature
  • 01 - Version
  • ChunkInfo[] - sorted by chunk key hash, then chunk key type
  • UntypedBlobKey[] - pack keys used in the chunk infos - sorted
  • 32 Chunk key hash (hash)
  • 1 Chunk key type (byte)
  • 1 Chunk key subtype (byte)
  • 1 Reserved - always 0 (byte)
  • 1 Reserved - always 0 (byte)
  • 4 Index of pack key (see map segment contents) (int32)
  • 4 Offset within pack (int32)
  • 4 Length within pack (int32)

Packs consist of

  • A fixed-length header, encrypted with AES-256, always 64 bytes (encrypted length)
  • A variable-length directory, encrypted with AES-256
  • One or more chunks, each encrypted with AES-256

For encryption, the CAS key of the archive is used.

  • 1d 05 02 - Signature
  • 01 - Version
  • Encrypted directory length (int32)
  • 1d 05 03 - Signature
  • 01 - Version
  • DirectoryEntry[] - sorted by offset
  • Offset (int32)
  • Length (int32)
  • 1d 05 04 - Signature
  • 01 - Version
  • Chunk key hash (hash)
  • Chunk type (byte)
  • Chunk subtype (byte)
  • Is the chunk’s contents compressed? (bool)
  • Does the content’s uncompressed hash equal the chunk key hash? (bool)
  • If it doesn’t:
    • The content’s uncompressed hash (hash)
  • If the contents is compressed:
    • The hash of the compressed contents (hash)
  • Length of compressed (if compressed) or uncompressed contents (int32)
  • The compressed (if compressed) or uncompressed contents

The content addressable storage (CAS) can store any number of objects with arbitrary length. The CAS internally uses the (encrypted) chunk storage as follows:

Small objects are written to the chunk store as a single chunk with

  • Chunk key hash: The hash of the object
  • Chunk type: CAS object (0)
  • Chunk subtype: CAS small object (0)

For large objects, a large object descriptor is written (see below) with

  • Chunk key hash: The hash of the object
  • Chunk type: CAS object (0)
  • Chunk subtype: CAS large object fragment list (1)

For each fragment of the large object, a chunk is written

  • Chunk key hash: The hash of the fragment
  • Chunk type: CAS large object fragment (1)
  • Chunk subtype: None (0)
  • 1d 05 05 - Signature
  • 01 - Version
  • The hashes of the object’s fragments (hash[])

Item lists stored in the CAS always begin with {"$":"itemList@ (ASCII). They don’t have a UTF-8 BOM.

{
"$": "itemList@1",
"items": [
{ ... },
{ ... },
{ ... }
]
}

Items can have the following type:

  • fileArchive
  • snapshot
  • folder
  • file
  • mailArchive
  • mailCollection
  • mailboxFolder
  • mailMessage
  • type: fileArchive
  • name: FileArchive
  • timestamp (Instant)
  • salt (byte[])
  • parent (Hash) (optional, hash of the previous root meta object, null = empty root meta object)
  • children (Hash)
  • type: snapshot
  • name: snapshot-group-name@instant
  • source (optional)
    • type: local
    • path (string)
  • size (int64)
  • folders (int32)
  • files (int32)
  • children (Hash)
  • childrenModified (bool) (optional, default: false)
  • type: folder
  • name (string)
  • size (int64)
  • folders (int32)
  • files (int32)
  • children (Hash)
  • type: file
  • name (string)
  • size (int64)
  • created (Instant) (optional)
  • modified (Instant) (optional)
  • contents (Hash)
  • thumbnail (Hash) (optional)
  • type: mailArchive
  • name: MailArchive
  • timestamp (Instant)
  • salt (byte[])
  • parent (Hash) (optional, hash of the previous root meta object, null = empty root meta object)
  • size (int64)
  • mailMessages (int32)
  • children (Hash)
  • type: mailCollection
  • name (string)
  • size (int64)
  • mailMessages (int32)
  • children (Hash)
  • type: mailboxFolder
  • name (string)
  • type: mailMessageItem
  • name (string)
  • size (int64)
  • added (Instant)
  • contents (Hash)
  • mailboxFolders (string[]) (optional)

Index files are encrypted with AES-256 as a whole, using the CAS key of the archive.

  • 1d 05 07 - Signature
  • 01 - Version
  • Apache Lucene.NET index file (byte[])