All formats

Library package format, version 1

Status: draft until app version 1.0 ships. Reference implementation: Packages/KuriosaKit/Sources/KuriosaStore. Reference files: Fixtures/golden/.

1. Package layout

A library is a directory holding manifest.json. On the device its name ends in .kuriosa; in a shared folder it is named Kuriosa Library or whatever the person renames it to (section 9):

<Name>.kuriosa/
  manifest.json          UTF-8 JSON, plaintext
  catalog.db.enc         KEF1 sealed SQLite database, context "catalog"
  media/<id>.enc         KEF1 sealed media file, context "media:<id>"
  media/<id>.thumb.enc   optional KEF1 sealed thumbnail, context "thumb:<id>"
  media/<id>.cutout.enc        optional KEF1 sealed cut-out of a photo, context "cutout:<id>"
  media/<id>.cutout-thumb.enc  thumbnail of the cut-out, context "cutout-thumb:<id>"

Photos are JPEG without metadata (see decision 0012); documents are PDF (redrawn without metadata) or JPEG; thumbnails are JPEG, 640 px on the long side. Thumbnails are derived data: readers may ignore them and writers may regenerate them.

A cut-out (decision 0016) is the photo's subject on a transparent background as PNG without metadata, at most 2,048 px on the long side with an even margin of 6 % of the subject's longer side; its thumbnail is PNG, 640 px. The media record says whether a cut-out exists and whether it is shown (catalog-schema.md). The original file never changes; a new cut-out replaces the cut-out files atomically.

Rotation and crop (decision 0021) are not files: they are columns of the media record, applied when a photo is drawn (catalog-schema.md, "Media edits"). Thumbnails and cut-outs are therefore always stored unrotated and uncropped.

A collection's own cover picture (catalog schema version 12, collection.cover_picture_id) is a photo file with its thumbnail but without a media record; it is carried in backups like every file in media/.

<id> is a lowercase UUID string. Media files are immutable. Writers replace files atomically (write to a temporary file, then rename).

A media file is in use while its media row is not deleted and belongs to an item that is not deleted, or while a collection that is not deleted names it in cover_picture_id. Other files of media/ named <id>.… are kept only so undo can bring them back; "Delete for good" (decision 0032) removes them with their media rows. Files with other names are left alone.

2. manifest.json

{
  "createdAt" : "2026-09-21T14:13:20Z",
  "formatVersion" : 1,
  "keySlots" : [
    {
      "algorithm" : "hkdf-sha256",
      "kind" : "recoveryKey",
      "salt" : "<base64, 16 bytes>",
      "wrappedKey" : "<base64, 60 bytes>"
    }
  ],
  "libraryID" : "<lowercase uuid>"
}

3. Key slots

The data key DK is 32 random bytes. Each slot stores DK encrypted with a key-encryption key KEK:

kindalgorithmKEK derivation
recoveryKeyhkdf-sha256HKDF-SHA256(IKM = recovery key bytes (20), salt = salt, info = UTF-8 kuriosa.keyslot.v1, L = 32)
passwordpbkdf2-sha256PBKDF2-HMAC-SHA256(P = UTF-8 of the NFC-normalized password, S = salt, c = iterations, dkLen = 32)

wrappedKey = AES-256-GCM(KEK, nonce = 12 random bytes, plaintext = DK, AAD = UTF-8 kuriosa.keyslot.v1:<kind>:<ownerID>), stored as nonce (12) || ciphertext (32) || tag (16). For a library, ownerID is the libraryID. The default PBKDF2 iteration count is 600,000.

4. Recovery key display form

20 random bytes encoded as 32 characters of Crockford Base32 (0123456789ABCDEFGHJKMNPQRSTVWXYZ, most significant bits first), shown as eight groups of four separated by -. Parsers ignore case, whitespace, and hyphens, map O→0 and I,L→1, and require exactly 32 characters.

5. KEF1 sealed file

All integers big endian.

OffsetLengthContent
04ASCII KEF1
44chunk size C (1 ≤ C ≤ 64 MiB; writers use 1 MiB)
88random nonce prefix P
16…records: ciphertext_i || tag_i (16)

6. Catalog

After decryption, catalog.db.enc is a standard SQLite 3 database file. Its schema is in catalog-schema.md; the version is PRAGMA user_version.

7. Backups

A backup is one file with the whole package, sealed with the same data key: backup-format.md.

8. Device key

On iOS the data key is stored in the Keychain (generic password, service app.kuriosa.library-key, account = libraryID). With the app lock on it is protected by SecAccessControl(.userPresence) and kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly; otherwise kSecAttrAccessibleWhenUnlockedThisDeviceOnly. It is never synced and not part of the package (decision 0013).

9. A library in a shared folder (decision 0035)

A library can live in a folder several devices share (iCloud Drive or another provider). The package keeps this format; Kuriosa names it Kuriosa Library (Kuriosa Library 2, … when the name is taken), without an ending, so file browsers show a plain folder name (issue #57). Packages made earlier are named Kuriosa Library.kuriosa. Rules for writers:

10. Combining two copies of a catalog

Catalog.merge (KuriosaStore) is the reference. Both copies are brought to the current schema; a copy written by a newer schema is refused. The result is the same whichever copy is merged into which, and merging again changes nothing.