Files
relicario/docs/FORMATS.md
adlee-was-taken 5e7023fcc1 docs: add scope headers + Next: footers to all tour docs
Each of the eight tour docs (README, DESIGN, docs/CRYPTO,
docs/FORMATS, docs/SECURITY, crates/relicario-core/ARCHITECTURE,
crates/relicario-cli/ARCHITECTURE, extension/ARCHITECTURE) now
declares its scope in a blockquote under its H1 and ends with a
single-line "Next:" pointer to the next doc in the canonical
reading order: README → DESIGN → CRYPTO → FORMATS → SECURITY →
core → cli → extension.

Also trimmed README's mid-section "Architecture" stub to a one-
paragraph pointer at DESIGN.md (was duplicating cross-codebase
content and referencing a non-existent docs/architecture/ tree).

Renamed docs/CRYPTO.md's H1 from "Relicario — Architecture" to
"Relicario — Crypto Pipeline" to match the file's renamed scope.

Spec: docs/superpowers/specs/2026-05-30-doc-structure-redesign-design.md
2026-05-30 15:36:46 -04:00

5.6 KiB

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), threat model around them (see SECURITY.md), or Rust struct internals (see ../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/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.


Next: SECURITY.md — the threat model.