# Relicario Wire Formats

> **Audience:** anyone implementing a compatible client or reading raw vault bytes. This doc owns the `.enc` blob layout, `params.json` / `salt` / `devices.json` / `revoked.json` shapes, the manifest JSON schema, the `.relbak` envelope, item-ID formats, and the settings JSON schema. **Does NOT own:** why these formats look this way (see [CRYPTO.md](CRYPTO.md)), threat model around them (see [SECURITY.md](SECURITY.md)), or Rust struct internals (see [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md)).

> Quick-reference for the load-bearing binary and JSON formats. Check this file before touching serialization, versioning, or storage layout code. Full diagrams and invariants live in the per-crate `ARCHITECTURE.md` files.

## Encrypted blob (`.enc` files)

Every encrypted file — `manifest.enc`, `settings.enc`, `items/<id>.enc`, `attachments/<item-id>/<aid>.enc` — uses the layout produced by `relicario_core::crypto::encrypt` (`crypto.rs`):

```
┌─────────┬────────────────────────┬──────────────────┬──────────────────┐
│ version │         nonce          │    ciphertext     │    auth tag      │
│ 1 byte  │       24 bytes         │    N bytes        │    16 bytes      │
│  0x02   │ random per write       │ XChaCha20 stream  │ Poly1305 MAC     │
└─────────┴────────────────────────┴──────────────────┴──────────────────┘
```

- `VERSION_BYTE = 0x02` (`crypto.rs:59`). Any blob starting with `0x01` is rejected with `UnsupportedFormatVersion { found: 0x01, expected: 0x02 }`.
- Minimum valid blob length: 41 bytes (1 + 24 + 0 + 16).
- Nonces are always fresh from `OsRng` — no caller-supplied nonces.
- Full diagram: `docs/CRYPTO.md` § "Encrypted File Format".

## `.relicario/params.json`

```json
{
  "format_version": 2,
  "aead": "xchacha20-poly1305",
  "salt_path": ".relicario/salt",
  "kdf": {
    "argon2_m": 65536,
    "argon2_t": 3,
    "argon2_p": 4
  }
}
```

Parsed via `ParamsFile { kdf: KdfParams }` in `session.rs`. The `kdf` nesting is intentional — `format_version`, `aead`, and `salt_path` co-exist for forward-compat probing. Do not flatten. Production defaults: `m=65536` (64 MiB), `t=3`, `p=4`. Tests use `m=256, t=1, p=1`.

## `.relicario/salt`

32 raw bytes. Not secret. Generated once at vault init via `OsRng`. Feeds Argon2id as the KDF salt.

## Manifest (`manifest.enc`)

Decrypts to JSON matching the `Manifest` struct (`manifest.rs`).

- **Schema version:** `MANIFEST_SCHEMA_VERSION = 2` (`manifest.rs:12`). v1 manifests (pre-typed-items) fail to parse and are not supported.
- **`ManifestEntry` fields** (declared order in `manifest.rs:21-38`): `id`, `type`, `title`, `tags`, `favorite`, `group`, `icon_hint`, `modified`, `trashed_at`, `attachment_summaries`. The `type` field is `r#type: ItemType` in Rust but serializes as the bare JSON key `"type"` (no serde rename — `r#` is just the raw-identifier escape). `group`, `icon_hint`, and `trashed_at` are `#[serde(skip_serializing_if = "Option::is_none")]`; `tags`, `favorite`, and `attachment_summaries` use `#[serde(default)]`.
- The manifest is rebuilt from scratch on every `upsert` — it can never drift from the source-of-truth item files.
- Supports case-insensitive title/tag search without decrypting any item.

## `.relicario/devices.json`

```json
[
  { "name": "laptop", "public_key": "<hex-encoded ed25519 public key>" }
]
```

An empty array (`[]`) puts the pre-receive hook in bootstrap mode (all pushes accepted). Both `devices.json` and `revoked.json` must be empty for bootstrap mode to activate — a non-empty `revoked.json` alone forces strict verification.

## `.relicario/revoked.json`

```json
[
  { "name": "old-laptop", "public_key": "<hex>", "revoked_at": 1746000000 }
]
```

Commits by `public_key` at or after `revoked_at` (Unix seconds) are rejected by the pre-receive hook. Commits before `revoked_at` remain valid (they were authorized at the time).

## Item IDs and Field IDs

| Kind | Length | Entropy | Source |
|---|---|---|---|
| `ItemId` | 16 hex chars | 64 bits | `OsRng` |
| `FieldId` | 16 hex chars | 64 bits | `OsRng` |
| `AttachmentId` | 32 hex chars | 128 bits | first 16 bytes (32 hex chars) of `SHA-256` over the plaintext |

`AttachmentId` is content-addressed — identical plaintexts deduplicate in git automatically. The 128-bit truncation (`ids.rs:59-69`) was widened from 64 bits per audit I2/B4 to put birthday-collision risk out of reach.

## `.relbak` backup format

A zstd-compressed tar archive containing a bare git clone of the vault. Designed for `relicario backup export/restore`.

Full spec: `docs/superpowers/specs/2026-04-27-relicario-import-export-design.md`.

## `ItemCore` JSON (internal)

`ItemCore` uses `#[serde(tag = "type")]` — the outer JSON object gets a `"type"` discriminator key. No `*Core` struct may have a field named `"type"` (use `"kind"` instead — see `CardKind`, `TotpKind`).

Full item type inventory: `crates/relicario-core/ARCHITECTURE.md` § "Module map".

## KDF input construction

The password fed to Argon2id is length-prefixed to prevent extension attacks:

```
u64_be(len(passphrase)) || passphrase_bytes || u64_be(32) || image_secret
```

NFC-normalized before hashing. Covered in `crypto.rs:229-236` and tested in `tests/format_v2.rs:44-54`.

---

**Next:** [SECURITY.md](SECURITY.md) — the threat model.
