Technical format

Azivault repository design

Repository root

The root contains small metadata files, a SQLite catalog, 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
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 === 1 for writes.
  3. Require minimumReaderVersion <= 1 for reads.
  4. Require the fixed version 1 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.

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": 1,
  "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": 1
}

Key material

The repository root key is 32 random bytes. For password recovery, derive a 32-byte wrapping key with PBKDF2-HMAC-SHA256 using the salt and iterations from recoveryWrappedKey, then open the AES-256-GCM wrappedKey. The result is the repository root key.

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

Purpose HKDF info
Blob encryption azivault.blob-encryption.v1
Blob identity azivault.blob-key.v1
Path lookup ID azivault.path-id.v1
Path encryption azivault.path-encryption.v1

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

Path privacy

Plaintext file names and relative paths are not stored unencrypted in catalog.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 with status = '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 should 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

Each encrypted blob starts with a 20-byte header, then one or more chunks. Integers are big-endian. The fixed chunk size is 1 MiB.

header =
  "BKBLOB2\n"                 // 8 bytes
  uint64 plaintext_size
  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, using the AAD above, decompress the plaintext with LZFSE, append chunks until final_flag is 1, and require the total bytes to match the header plaintext size.

Native API note: blob sealing and opening use CryptoKit AES.GCM and Foundation's LZFSE NSData.compressed / NSData.decompressed APIs.

Checkpoints

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

{
  "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 should 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 Swift URLSession for network requests. Finder restore browsing is implemented with Apple's File Provider APIs, including NSFileProviderReplicatedExtension, NSFileProviderEnumerator, and NSFileProviderSearchEnumerator.

Client rules

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