All formats

Backup file format, version 1

Status: draft until app version 1.0 ships. Reference implementation: Packages/KuriosaKit/Sources/KuriosaStore/Backup/BackupFile.swift; a reader in JavaScript, free to use, is website/src/tool/kuriosa-open.js, published on kuriosa.app with the tool "Open a backup without Kuriosa" (export-everything.md, section 9). Reference file: Fixtures/golden/backup-v1/ (decision record 0018).

A backup is one file with a whole library: the catalog, every photo and document, thumbnails, and cut-outs. It stays encrypted with the library's own data key, so it opens with the same keys as the library: the device key on the iPhone that made it, or the recovery key anywhere else.

1. File

OffsetLengthContent
04ASCII KBK1
44header length H (1 ≤ H ≤ 1 MiB)
8Hheader, UTF-8 JSON (section 2)
8 + H…entries (section 3), the sealed index last (section 4)

2. Header

{
  "backupID" : "<lowercase uuid>",
  "createdAt" : "2026-10-01T12:00:00Z",
  "formatVersion" : 1,
  "manifest" : { … the library's manifest.json, see library-format.md … }
}

The header is plaintext and not authenticated by itself. The sealed index (section 4) repeats backupID and createdAt, and every key slot is bound to the library ID, so a changed header makes reading fail.

3. Entries

Each entry is:

LengthContent
2path length P
Ppath, UTF-8, relative to the package root
8data length N
Ndata

Except for the index, the data is a file of the library package copied byte for byte (already KEF1 sealed, see library-format.md). Allowed paths:

where <id> is a lowercase UUID. Readers refuse any other path, so no entry can be written outside the package. manifest.json is not an entry; it is in the header.

Writers put the catalog first and the media files after it, sorted by path. Readers check the data key with the catalog as soon as it is read (KEF1 context catalog), before copying media files.

4. Sealed index

The last entry has the path backup-index.enc. Its data is a KEF1 sealed file with the library data key and the context backup-index:<backupID>. The plaintext is UTF-8 JSON:

{
  "backupID" : "<same as the header>",
  "createdAt" : "<same as the header>",
  "entries" : [
    { "path" : "catalog.db.enc", "sha256" : "<lowercase hex>", "size" : 204800 },
    { "path" : "media/<id>.enc", "sha256" : "<lowercase hex>", "size" : 1048576 }
  ]
}

entries lists every entry before the index, in file order, with the SHA-256 of its stored bytes. No bytes may follow the index.

A reader accepts the backup only if the index decrypts, its backupID and createdAt equal the header's, and its entries equal the entries actually read (same paths, order, sizes, and hashes). This detects changed, missing, added, and swapped files, and a file from another backup.

5. Restoring

  1. Read the header. Get the data key: the device key stored for manifest.libraryID, or the user's recovery key with the manifest's recovery key slot.
  2. Unpack the entries into a new package folder and verify the index. On any error, delete the folder.
  3. Write manifest.json from the header. If the device's current library has the same library ID, keep its manifest instead: it may hold a newer recovery key.
  4. Open the unpacked package once (decrypt and read the catalog). Only then replace the current library with it.

The app keeps the replaced library on the device until the next restore, so a restore can be undone ("Undo restore" in Settings).

6. What the plaintext reveals

The header and the entry framing reveal the library ID, when the library and the backup were made, the number of media files, and their sizes. They reveal no titles, values, photos, or other collection data. The same is true of a library package on disk.