Files
relicario/FORMATS.md
adlee-was-taken 210232d156 docs: fix crypto/format drift — version byte 0x02, AttachmentId 32 hex, DCT 5-50
Punch items from doc audit:
- docs/ARCHITECTURE.md: encrypted file format diagram said version byte
  0x01; actual VERSION_BYTE is 0x02 (crypto.rs:59) and 0x01 is rejected
  with UnsupportedFormatVersion.
- docs/ARCHITECTURE.md: DCT embedding diagram said "Repeat secret 20+
  times" and "positions 4-15"; actual is MIN_COPIES (5) to 50 copies
  chosen by capacity, embedded in zig-zag positions 6-17
  (imgsecret.rs:78, 99-104, 530-537).
- FORMATS.md: AttachmentId table said 16 hex chars / 8 bytes; actual is
  32 hex chars / first 16 bytes of SHA-256 (ids.rs:59-69).
- FORMATS.md: ManifestEntry schema missing r#type field; updated to list
  all ten fields in declared order with serde decorations noted
  (manifest.rs:21-38).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:24:40 -04:00

5.0 KiB

Relicario Wire Formats

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/ARCHITECTURE.md § "Encrypted File Format".

.relicario/params.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

[
  { "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

[
  { "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.