Files
relicario/docs/FORMATS.md
adlee-was-taken 0cd417ded7 docs(org): complete A5 living-docs sweep (item CRUD merged) + dead_code cleanup
Extends the A5 pre-stage now that dev-b's full B-stream (item CRUD + all 19
org subcommands) merged to main (7392795). Living docs:
- FORMATS/CRYPTO/SECURITY/DESIGN: flip the item-CRUD "pending Dev-B" markers to
  shipped; SECURITY audit vocabulary moves item-* actions to live.
- crates/relicario-cli/ARCHITECTURE.md: full 19-subcommand surface (12 admin +
  7 item CRUD), accurate OrgAddKind scope (Login/SecureNote/Identity).
- STATUS.md: enterprise-org-vault landed section (merged 7392795) + tracked
  follow-ups + honest known-limitations; correct spec citation.
- ROADMAP.md: backend-complete row + phase-2 follow-ups.
- CHANGELOG.md: finalize the enterprise-org-vault Unreleased section (item CRUD
  into Added; Card/Key/Document/Totp + extension + phase-2 into Deferred).

Code (PM-directed dead_code fixes): wire device::current_device_seed by removing
the identical duplicate private fn in org_session.rs (de-dup); #[allow(dead_code)]
+ justification on org_session org_meta_path/load_meta (API completeness, no
command consumes org.json yet). Also silence a 3rd pre-existing test-only warning
(unused relicario() helper in tests/org_init_signing.rs).

Honest deferrals kept explicit throughout: Card/Key/Document/Totp org add/edit
parity, extension org switch/read (Dev-D) + writes, phase-2 (SSO/LDAP, read
audit, per-collection subkeys, HTTP plane). Full workspace cargo test green,
zero warnings. All cited code constants pinned file:line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy
2026-06-20 15:54:51 -04:00

9.8 KiB
Raw 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

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.

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.

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.