Files
relicario/docs/FORMATS.md
adlee-was-taken db0ab1d82e docs(formats): org collection-scoped attachment layout + default cap
Document the attachments/<slug>/<item-id>/<att-id>.enc layout (exactly 3
segments, slug-authorized by the pre-receive hook, never decrypted
server-side) and DEFAULT_ORG_ATTACHMENT_MAX_BYTES = 10 MiB, citing
org_session.rs:24 and the mirrored personal default settings.rs:116.
2026-06-20 21:08:23 -04:00

11 KiB
Raw Permalink Blame History

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/CRYPTO.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).

Org vault repo formats

The org vault is a separate git repository alongside the personal vault. It is not nested inside .relicario/. Its layout:

org.json                              # OrgMeta (schema_version, org_id, display_name, created_at)
members.json                          # PUBLIC/unencrypted member directory
collections.json                      # collection definitions
keys/<member-id>.enc                  # org master key wrapped to that member's device key
manifest.enc                          # OrgManifest (schema_version 1, per-member-filtered)
items/<collection-slug>/<item-id>.enc # collection-scoped item blobs
attachments/<collection-slug>/<item-id>/<att-id>.enc # Document attachment blobs (collection-scoped)

org.json — OrgMeta

Unencrypted JSON (OrgMeta, org.rs:164). schema_version: 1 (org.rs:174). Fields: schema_version, org_id, display_name, created_at (Unix seconds).

members.json — OrgMembers

Unencrypted JSON array of OrgMember records (org.rs:72); container type OrgMembers carries schema_version: 1 (org.rs:93). Per-member fields: member_id (16 lowercase hex chars), display_name, role (one of owner | admin | member), ed25519_pubkey (OpenSSH wire string), collections (array of granted slug strings), added_at, added_by. Roles are not secrets — authorization to read this file is not required to verify signatures.

collections.json — OrgCollections

Unencrypted JSON; schema_version: 1 (org.rs:138). Contains a list of CollectionDef records (org.rs:123). Validation (org.rs:145) rejects slugs that are empty, contain /, or equal ..

keys/<member-id>.enc — wrapped org master key

Binary blob; NOT a standard .enc blob. Layout (org.rs:264):

┌──────────────────────────┬─────────┬────────┬──────────────────────┐
│ ephemeral_x25519_pubkey  │ version │ nonce  │   ciphertext + tag   │
│        32 bytes          │ 1 byte  │24 bytes│       N + 16 bytes   │
└──────────────────────────┴─────────┴────────┴──────────────────────┘
  • The wrapping key is SHA-256(dh_shared || ephemeral_pubkey || recipient_pubkey) (org.rs:278281), held in Zeroizing<Vec<u8>>.
  • The inner AEAD (version || nonce || ciphertext+tag) is produced by crate::crypto::encrypt — the same XChaCha20-Poly1305 framing used for personal .enc blobs (see Encrypted blob above). VERSION_BYTE = 0x02 applies here too.
  • The X25519 private scalar is derived from the device ed25519 seed via SHA-512(seed)[..32] with RFC 7748 clamping (org.rs:242). Argon2id is not involved — the wrapping key is derived entirely from the X25519 DH exchange.

manifest.enc — OrgManifest

Encrypted with the org master key using crypto::encrypt (standard .enc framing). Decrypts to OrgManifest JSON (org.rs:199); schema_version: 1 (org.rs:206). Each OrgManifestEntry (org.rs:185) carries: id, type, title, tags, modified, trashed_at, and a collection slug field. The collection field distinguishes this type from ManifestEntry in the personal vault.

Contrast with the personal vault manifest: Manifest uses MANIFEST_SCHEMA_VERSION = 2 (manifest.rs:12) and ManifestEntry has no collection field. The two types are distinct and do not share a schema.

items/<collection-slug>/<item-id>.enc

Standard .enc blob (see Encrypted blob above), encrypted under the org master key. The blob itself does not name its collection — the directory path segment carries the slug. This allows the pre-receive hook (relicario-server) to authorize a write by path segment without decrypting the blob.

These blobs are written and read by the relicario org item commands (org add / get / list / edit / rm / restore / purge), all collection-scoped and grant-enforced. org add currently creates Login / SecureNote / Identity items; get / list display any item type present.

attachments/<collection-slug>/<item-id>/<att-id>.enc

Standard .enc blob (see Encrypted blob above), encrypted under the org master key — the encrypted file payload of a Document item. As with item blobs, the blob does not name its collection; the leading <collection-slug> path segment carries it, so the pre-receive hook (relicario-server, classify_path) authorizes the write by slug without decrypting — reusing the same grant + slug-existence check as the items/ branch. The path is exactly three segments after attachments/ (<collection-slug>/<item-id>/<att-id>.enc); the hook rejects any other shape (segment-count and .-free slug guards). <att-id> is the content-addressed AttachmentId (see Item IDs and Field IDs below).

Per-attachment size is capped at DEFAULT_ORG_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024 (10 MiB) (org_session.rs:24), mirroring the personal-vault default AttachmentCaps::per_attachment_max_bytes (settings.rs:116). Org vaults have no settings.enc, so this cap is a fixed default rather than per-org configurable. Blobs are persisted / read / removed by UnlockedOrgVault::save_attachment / load_attachment / remove_item_attachments (org_session.rs:137, :147, :156). The storage primitives back the org Document item type; the org add document / Document-edit commands that produce these blobs land in v0.8.1 (see the item-type-parity note below).

TODO (extension follow-up): extension UI for browsing and editing org vault items. Deferred: org add / edit parity for Card / Key / Document / Totp item types (landing in v0.8.1; Document file payloads use the attachment layout above).

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.