Technical format

Azivault repository design

Repository root

The root contains small metadata files, a SQLite catalog, a path-free Photos snapshot index, encrypted blob objects, path-free run history, and transient run state. S3-compatible storage uses the same keys as the local directory layout.

repo.json
plans.json
catalog.sqlite
photos/index.json
blobs/<first-two-hex>/<64-char-hex-blob-key>
history/runs/v1/<encoded-plan-id>/<yyyy>/<mm>/<yyyy-mm-dd>.json
history/run-details/v1/<encoded-plan-id>/<encoded-run-uuid>/manifest.json
history/run-details/v1/<encoded-plan-id>/<encoded-run-uuid>/chunks/<chunk-number>.bin
run-state/

Reader startup

  1. Open repo.json. A missing file is not a supported repository.
  2. Require formatVersion to be a supported writable version.
  3. Require minimumReaderVersion <= 3 for this reader.
  4. Require the fixed version 1 through version 3 layout: catalog.sqlite, blobs, hmac-sha256, and checkpoint storage in blobs.
  5. Require encrypted repositories.
  6. Open catalog.sqlite without creating or migrating it, and require PRAGMA user_version = 1.

Version 1 and version 2 repositories remain readable. Photos plans require version 2. A folder backup upgrades an earlier repository to version 3 before writing file-system metadata or symbolic-link entries, then raises minimumReaderVersion so an older reader refuses the repository safely.

repo.json

repo.json is written before catalog data. It is the compatibility and crypto gate for every reader.

{
  "createdAt": "2026-05-21T20:00:00Z",
  "formatVersion": 3,
  "layout": {
    "blobKeyAlgorithm": "hmac-sha256",
    "blobsPath": "blobs",
    "catalogPath": "catalog.sqlite",
    "checkpointStorage": "blobs"
  },
  "encryption": {
    "blobAlgorithm": "AES-256-GCM",
    "compressionAlgorithm": "LZFSE",
    "keyDerivationAlgorithm": "HKDF-SHA256",
    "recoveryWrappedKey": {
      "algorithm": "AES-256-GCM",
      "iterations": 210000,
      "kdf": "PBKDF2-HMAC-SHA256",
      "salt": "base64",
      "wrappedKey": "base64"
    },
    "version": 1
  },
  "minimumReaderVersion": 3
}

Key material

The repository root key is 32 random bytes. For password recovery, derive a 32-byte wrapping key with PBKDF2-HMAC-SHA256 using thesalt and iterations fromrecoveryWrappedKey, then open the AES-256-GCMwrappedKey. The result is the repository root key.

Derive subkeys with HKDF-SHA256 using saltazivault.repository.v1, the repository root key as input key material, and a 32-byte output length:

The following table lists the derived repository keys.

PurposeHKDF info
Blob encryptionazivault.blob-encryption.v1
Blob identityazivault.blob-key.v1
Path lookup IDazivault.path-id.v1
Path encryptionazivault.path-encryption.v1

Native API note: Azivault uses Swift CryptoKit for HKDF-SHA256, HMAC-SHA256, SHA-256, and AES-GCM; CommonCryptofor PBKDF2-HMAC-SHA256; and SecuritySecRandomCopyBytes for root-key randomness.

Path privacy

Plaintext file names and relative paths are not stored unencrypted incatalog.sqlite, plans.json, or remote object keys. Checkpoint manifests are also stored encrypted; after unlock, the decrypted checkpoint JSON contains the relative paths needed to browse and restore a snapshot. A relative path is normalized by trimming leading and trailing slashes. Its lookup ID is lowercase hex HMAC-SHA256 over the normalized UTF-8 path using the path ID key.

Display paths are AES-GCM combined payloads using the path encryption key. The combined representation is nonce, ciphertext, and tag. A client needs the repository key to browse, search, restore, or verify encrypted paths.

plans.json

plans.json is an import index, not restore authority. It helps clients discover backup sets and schedule intent without needing the original app database. A missing plans.json is an empty index only after repo.json has been validated.

{
  "version": 1,
  "plans": [
    {
      "createdAt": "2026-05-21T20:00:00Z",
      "destinationPathCiphertext": "base64-aes-gcm-combined-payload",
      "name": "Documents",
      "planID": "9f4c8d8b-7c2d-4e4b-8b31-fc4f2b5b8d2a",
      "settings": {
        "cloudFilePolicy": "treatAsError",
        "networkPolicy": "avoidExpensive",
        "powerPolicy": "avoidLowPower",
        "scheduleFrequency": "daily",
        "scheduleHour": 9,
        "scheduleMinute": 0,
        "scheduleMode": "automatic",
        "scheduleWeekday": 2,
        "skipExternalSymlinks": true,
        "version": 2
      },
      "sourcePathCiphertext": "base64-aes-gcm-combined-payload",
      "updatedAt": "2026-05-21T20:00:00Z"
    }
  ]
}

Plan settings must not contain security-scoped bookmarks, local credentials, recovery passwords, Keychain identifiers, provider tokens, S3 credentials, signed URLs, or plaintext source and destination paths.

Catalog schema

The catalog is SQLite with PRAGMA user_version = 1. It is the restore source of truth. Only rows in runs withstatus = 'completed' are restorable.

runs(
  id INTEGER PRIMARY KEY,
  run_uuid TEXT NOT NULL UNIQUE,
  plan_id TEXT NOT NULL,
  started_at REAL NOT NULL,
  finished_at REAL,
  source_path_ciphertext BLOB NOT NULL,
  dest_path_ciphertext BLOB NOT NULL,
  notes TEXT,
  case_insensitive INTEGER NOT NULL DEFAULT 0,
  hash_algorithm TEXT NOT NULL DEFAULT 'sha256',
  engine_version TEXT,
  source_volume_uuid TEXT,
  dest_volume_uuid TEXT,
  status TEXT CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
  files_total_count INTEGER,
  files_total_bytes INTEGER,
  files_copy_count INTEGER,
  files_replace_count INTEGER,
  files_skipped_count INTEGER,
  files_deleted_count INTEGER
)

files_current(
  plan_id TEXT NOT NULL,
  path_id TEXT NOT NULL,
  rel_path_ciphertext BLOB NOT NULL,
  rel_path_norm_ciphertext BLOB NOT NULL,
  blob_key TEXT NOT NULL,
  size_bytes INTEGER NOT NULL,
  created_at REAL,
  modified_at REAL NOT NULL,
  content_id INTEGER,
  last_seen_run_id INTEGER NOT NULL,
  deleted INTEGER NOT NULL DEFAULT 0,
  deleted_at REAL,
  PRIMARY KEY(plan_id, path_id)
)

file_events(
  id INTEGER PRIMARY KEY,
  run_id INTEGER NOT NULL,
  plan_id TEXT NOT NULL,
  path_id TEXT NOT NULL,
  rel_path_ciphertext BLOB NOT NULL,
  rel_path_norm_ciphertext BLOB NOT NULL,
  op TEXT NOT NULL,
  blob_key TEXT,
  size_bytes INTEGER,
  created_at REAL,
  modified_at REAL,
  content_id INTEGER,
  deleted INTEGER NOT NULL DEFAULT 0,
  ts REAL NOT NULL
)

checkpoints(
  plan_id TEXT NOT NULL,
  run_id INTEGER NOT NULL,
  manifest_blob_key TEXT NOT NULL,
  created_at REAL NOT NULL,
  PRIMARY KEY(plan_id, run_id)
)

Native API note: the app talks to the system SQLite library through Swift's SQLite3 module and stores catalog rows using Foundation Date, Data, FileManager, and FileHandle primitives.

Blob keys and paths

File blobs are content-addressed with lowercase hex HMAC-SHA256 over the plaintext file bytes using the blob identity key. The object path is:

blobs/<first-two-hex-chars>/<64-char-hex-blob-key>

Writers stage blob data and then atomically install it. Readers must verify that an existing encrypted blob opens successfully and recomputes to the key named by its path. If a blob key is referenced by the catalog but missing from blobs/, the repository is incomplete.

Checkpoint manifests also live under blobs/, but their blob key is SHA-256 over the encrypted checkpoint blob bytes. File-blob keys are the HMAC-SHA256 plaintext identifiers described above.

Encrypted blob container

An encrypted blob uses a known-size BKBLOB2 header or the streaming BKBLOB3 header used for PhotoKit resources. Each header is followed by one or more chunks. Integers are big-endian. The fixed chunk size is 1 MiB.

known-size header =
  "BKBLOB2\n"                 // 8 bytes
  uint64 plaintext_size
  uint32 chunk_size            // 1048576

streaming header =
  "BKBLOB3\n"                 // 8 bytes
  uint32 chunk_size            // 1048576

chunk =
  uint32 sealed_size
  uint32 plaintext_chunk_size
  uint8 final_flag             // 0 or 1
  AES_GCM(compress_lzfse(plaintext_chunk))

AES-GCM additional authenticated data is:

header || uint64 chunk_index || uint32 plaintext_chunk_size || uint8 final_flag

To restore a file, open each chunk with the blob encryption key, use the AAD shown here, decompress the plaintext with LZFSE, and append chunks until final_flag is 1. ForBKBLOB2, require the total bytes to match the header plaintext size. BKBLOB3 ends at the authenticated final chunk because its size isn't known before PhotoKit streams the resource.

Native API note: blob sealing and opening use CryptoKitAES.GCM and Foundation's LZFSENSData.compressed / NSData.decompressed APIs.

Checkpoints

Checkpoint manifests are stored as blobs and referenced fromcheckpoints.manifest_blob_key. The decrypted checkpoint JSON has this shape:

Photos snapshots

Repository format version 2 adds first-class Photos snapshots. A repository can contain folder plans, Photos plans, or both.photos/index.json is a path-free snapshot index rather than a Photos database copy.

{
  "version": 1,
  "snapshots": [
    {
      "planID": "photo-library",
      "snapshotUUID": "UUID",
      "createdAt": "2026-09-04T12:00:00Z",
      "manifestBlobKey": "64-char-hex",
      "assetCount": 12000,
      "resourceCount": 14500,
      "totalBytes": 9876543210,
      "coverage": {
        "coreMetadata": true,
        "userAlbums": true,
        "albumFolders": true,
        "titlesCaptionsAndKeywords": true
      }
    }
  ]
}

The public index contains counts, the encrypted manifest blob key, and non-sensitive coverage flags. It does not contain PhotoKit identifiers, filenames, album names, locations, titles, captions, keywords, or other user metadata.

Each snapshot references an encrypted root descriptor and bounded encrypted asset and collection shards. The shards preserve every PhotoKit resource exposed for the selected assets, supported metadata, user-created folder and album relationships, membership, asset order, and the PhotoKit change token used for incremental continuity.

A Photos snapshot becomes restorable only after every selected resource and the encrypted manifest are durable. Restore recreates new Photos assets, supported dates and locations, favorite and hidden state, and user-created album organization. Coverage flags disclose metadata that public Apple interfaces cannot recreate.

Version 3 file-system metadata

Repository format version 3 gives folder snapshots encrypted metadata blobs and an explicit symbolic-link entry kind. Metadata can include POSIX mode, ownership IDs, BSD flags, dates, extended attributes, ACL text, a symbolic-link target, and validated sparse-file hole ranges.

These fields remain encrypted and content-addressed. Restore applies metadata conservatively and reports unsupported or rejected attributes instead of treating a partially reconstructed file as an exact copy.

{
  "version": 1,
  "createdAt": 1782072000.0,
  "files": [
    {
      "relPath": "Reports/Q1.pdf",
      "relPathNorm": "Reports/Q1.pdf",
      "kind": "file",
      "blobKey": "64-char-hex",
      "sizeBytes": 12345,
      "createdAt": 1782070000.0,
      "modifiedAt": 1782071000.0,
      "contentID": 123456
    }
  ]
}

The manifest plaintext is encrypted inside the blob container. If a referenced checkpoint is corrupt, a reader must fail loudly rather than silently falling back to event replay.

Restore algorithm

  1. Validate repo.json and unlock the repository key.
  2. Open catalog.sqlite without creating or migrating it, and validate user_version.
  3. Select the requested plan and a completed run from runs.
  4. If a checkpoint exists for that plan/run, decrypt and use its manifest as the file snapshot.
  5. Otherwise, reconstruct each path by taking the latest file_events row for each path_id at or before the run, excluding deleted rows.
  6. For a file entry, load blobs/<prefix>/<blob_key>, verify and decrypt the blob container, and write the plaintext to the selected restore destination.
  7. Never restore from running, failed, or cancelled runs.

History objects

history/runs/v1/ stores compact daily JSON shards for backup history and import UX. It is path-free and may include failed or interrupted runs. It does not define file membership for restore.

history/run-details/v1/ stores encrypted run-detail UI records. Manifest objects are path-free; chunk objects are encrypted and may contain file names, relative paths, issue details, and log rows after unlock. These records are searchable UI metadata, not restore authority.

Native API note: S3-compatible repositories use SwiftURLSession for network requests. Finder restore browsing is implemented with Apple's File Provider APIs, includingNSFileProviderReplicatedExtension,NSFileProviderEnumerator, andNSFileProviderSearchEnumerator.

Client rules

If you just want to recover files, use theazi CLI manual instead of this implementation reference.