Technical format
Azivault repository design
This page describes the v1 format of the Azivault backup repository.
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
- Open
repo.json. A missing file is not a supported repository. - Require
formatVersion === 1for writes. - Require
minimumReaderVersion <= 1for reads. - Require the fixed version 1 layout:
catalog.sqlite,blobs,hmac-sha256, and checkpoint storage inblobs. - Require encrypted repositories.
- Open
catalog.sqlitewithout creating or migrating it, and requirePRAGMA 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
- Validate
repo.jsonand unlock the repository key. - Open
catalog.sqlitewithout creating or migrating it, and validateuser_version. - Select the requested plan and a
completedrun fromruns. - If a checkpoint exists for that plan/run, decrypt and use its manifest as the file snapshot.
- Otherwise, reconstruct each path by taking the latest
file_eventsrow for eachpath_idat or before the run, excluding deleted rows. - For a file entry, load
blobs/<prefix>/<blob_key>, verify and decrypt the blob container, and write the plaintext to the selected restore destination. - Never restore from
running,failed, orcancelledruns.
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
- Validate versions and fixed layout values before reading catalog data.
- Open catalogs without creating or migrating them for restore, list, verify, and export operations.
- Treat
catalog.sqliteplusblobs/as restore authority. - Use
plans.jsonandhistory/only for discovery, import, and UI context. - Reject unsupported encryption, compression, KDF, or chunk-size metadata.
- Do not write plaintext file names, source paths, destination paths, credentials, passwords, bookmarks, provider tokens, or signed URLs into repository objects.
If you just want to recover files, use the
azi CLI manual instead of this
implementation reference.