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 (merged7392795) + 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
9.8 KiB
Relicario Wire Formats
Audience: anyone implementing a compatible client or reading raw vault bytes. This doc owns the
.encblob layout,params.json/salt/devices.json/revoked.jsonshapes, the manifest JSON schema, the.relbakenvelope, 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.mdfiles.
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 with0x01is rejected withUnsupportedFormatVersion { 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. ManifestEntryfields (declared order inmanifest.rs:21-38):id,type,title,tags,favorite,group,icon_hint,modified,trashed_at,attachment_summaries. Thetypefield isr#type: ItemTypein Rust but serializes as the bare JSON key"type"(no serde rename —r#is just the raw-identifier escape).group,icon_hint, andtrashed_atare#[serde(skip_serializing_if = "Option::is_none")];tags,favorite, andattachment_summariesuse#[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:278–281), held inZeroizing<Vec<u8>>. - The inner AEAD (
version || nonce || ciphertext+tag) is produced bycrate::crypto::encrypt— the same XChaCha20-Poly1305 framing used for personal.encblobs (see Encrypted blob above).VERSION_BYTE = 0x02applies 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.