diff --git a/docs/superpowers/specs/2026-06-06-relicario-enterprise-org-vault-design.md b/docs/superpowers/specs/2026-06-06-relicario-enterprise-org-vault-design.md new file mode 100644 index 0000000..fb160d3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-06-relicario-enterprise-org-vault-design.md @@ -0,0 +1,348 @@ +# Relicario Enterprise Org Vault — Design Spec + +**Scope:** Multi-user organizational vault for security-conscious self-hosting shops. Covers the git-native org model, cryptographic key-wrapping scheme, access control, admin CLI surface, audit trail, and testing strategy. Does not cover SSO/SAML, live SIEM streaming, or the HTTP management plane (deferred to a later server-tier spec). + +**Next:** `docs/superpowers/specs/2026-05-02-relay-server-design.md` (relay server — future phase 2 management plane) + +--- + +## Target Audience + +Security-focused organizations that self-host their entire stack: infosec shops, security consultancies, law firms handling privileged client data, small financial firms. Key requirements: + +- Full air-gap capability — no mandatory internet connectivity +- Cryptographically provenance-linked audit trail +- Personal vaults remain isolated from org vault (separate cryptographic domains) +- Member offboarding with clean key revocation +- Deployable without an IdP, SSO provider, or cloud dependency + +--- + +## Architecture Overview + +An org vault is a second git repository alongside each member's personal vault. Personal and org vaults are cryptographically isolated — the personal vault's two-factor KDF (passphrase + image → Argon2id → master key) is completely untouched by org operations. + +``` +Personal: ~/.config/relicario/personal/ → personal git repo (passphrase + image → Argon2id → master key) +Org: ~/.config/relicario/acme-org/ → org git repo (org master key, wrapped per-member) +``` + +**Two cryptographic domains, one CLI and extension.** + +The org vault uses a random 256-bit **org master key** to encrypt all org items. Each authorized member receives a copy of the org master key wrapped (XChaCha20-Poly1305) to their existing ed25519 device public key — converted to X25519 for the Diffie-Hellman step. To open the org vault, a member uses their device private key to unwrap their copy of the org master key, then decrypts items exactly as today. + +**Key security properties:** + +- Member departure = delete their `keys/.enc` + rotate the org master key. No passphrase change, no re-imaging. +- Every write to the org repo is a signed git commit — the audit log is the repo history. +- The pre-receive hook in `relicario-server` is the policy enforcement point. +- Fully air-gapped: the org repo is just git, push/pull over SSH. +- A compromised org master key does not expose personal vault items. + +**Phase 2 (not in this spec):** live SIEM streaming, SSO/SAML, HTTP management plane via the `relicario-server` relay skeleton. + +--- + +## Data Model + +The org repo has a defined on-disk schema. The pre-receive hook rejects pushes that violate it. + +``` +acme-org/ +├── org.json # org identity: name, org_id, created_at, schema_version +├── members.json # user directory (unencrypted — roles are not secrets) +├── collections.json # collection definitions +├── keys/ +│ └── .enc # org master key wrapped to each member's X25519 public key +├── manifest.enc # encrypted org manifest (item index + collection membership) +└── items/ + └── .enc # encrypted items (identical format to personal vault items) +``` + +### `org.json` + +```json +{ + "schema_version": 1, + "org_id": "<16-char hex>", + "display_name": "Acme Security", + "created_at": 1748000000 +} +``` + +### `members.json` + +Public and unencrypted — readable without the org master key. Roles are not secrets; the key material is in `keys/`. + +```json +{ + "schema_version": 1, + "members": [ + { + "member_id": "<16-char hex>", + "display_name": "Alice", + "role": "owner", + "ed25519_pubkey": "", + "collections": ["prod-infra", "shared-tools"], + "added_at": 1748000000, + "added_by": "" + } + ] +} +``` + +`role` is one of `owner`, `admin`, `member`. `collections` is the list of collection slugs this member is granted access to. `member_id` is a 16-char lowercase hex string generated from 64 bits of `OsRng` entropy — the same convention as `ItemId` and `FieldId` in `relicario-core/src/ids.rs`. + +### `collections.json` + +```json +{ + "schema_version": 1, + "collections": [ + { + "slug": "prod-infra", + "display_name": "Production Infrastructure", + "created_by": "", + "created_at": 1748000000 + } + ] +} +``` + +### `keys/.enc` + +The org master key (32 bytes) encrypted with XChaCha20-Poly1305 using a key derived via X25519 from the member's ed25519 public key (converted to X25519 using the standard Ristretto/curve25519 mapping). One file per member. Deleting this file + rotating the org master key constitutes clean revocation. + +### `manifest.enc` + +Encrypted with the org master key. Same schema as the personal vault manifest but each item entry includes a `collection` field (the slug). The manifest is the only place collection membership is stored for items — item blobs carry no collection metadata. + +### `items/.enc` + +Identical `.enc` format to personal vault items (XChaCha20-Poly1305, random 24-byte nonce, no Argon2id — the org master key is used directly). Item IDs follow the same 16-char hex convention. + +--- + +## Access Control + +### Roles + +| Role | Permissions | +|---|---| +| **Owner** | All operations. Add/remove admins. Rotate org master key. Delete org. Transfer ownership. | +| **Admin** | Add/remove members. Create/delete collections. Grant/revoke collection access. Read all collections. Cannot remove owners or other admins. | +| **Member** | Read/write items in granted collections only. Cannot see items outside their grant list. | + +### Collection Access + +Access grants are stored in the member's `collections` array in `members.json`. No separate ACL file. An admin edits the member record and commits; the pre-receive hook validates the commit author has admin or owner role. + +### Enforcement Layers + +Access control is enforced at two layers: + +1. **Manifest filtering** — the CLI and extension filter the decrypted manifest to items whose `collection` slug appears in the authenticated member's grant list. Members never receive item blobs for collections they are not granted. + +2. **Pre-receive hook** — rejects pushes where the committing device's member record does not have write access to the collections whose item files are modified. + +### Known Limitation + +Every member holds the same org master key (wrapped to their device key). A member with a local copy of the org repo and their device key can technically decrypt any item blob — the org master key is not scoped per-collection. Access control is enforced by the CLI, extension, and pre-receive hook, not by cryptographic isolation between collections. + +For items requiring true cryptographic separation within a security shop, place them in a separate org vault. Per-collection subkeys are an explicit non-goal for this phase. + +### "Hide value" (phase 2) + +Bitwarden and LastPass support "autofill without revealing plaintext." This requires per-item subkeys or a relay that mediates autofill. Out of scope for phase 1 — documented as a known gap. + +--- + +## Admin Operations + +All org management uses `relicario org `. + +``` +relicario org init --name "Acme Security" + Create org repo, generate org master key, add caller as owner. + +relicario org add-member --key --name Alice --role member + Wrap org master key to Alice's X25519 key. Write keys/.enc + update members.json. + +relicario org remove-member + Delete keys/.enc, update members.json. Prompts to run org rotate-key immediately. + +relicario org set-role admin|member + Update role in members.json. Owners only for admin promotion. + +relicario org create-collection --name "Display Name" + Append to collections.json. + +relicario org grant + Add slug to member's collections array in members.json. + +relicario org revoke + Remove slug from member's collections array. + +relicario org rotate-key + Generate new org master key. Re-wrap for all current members. Re-encrypt manifest. + Item blobs do not need re-encryption (only the manifest references them). + Does git pull --rebase before generating to detect concurrent rotations. + +relicario org status + Print members, roles, and collection grants. No decryption required. + +relicario org audit [--since ] [--member ] [--collection ] [--action ] [--format json] + Query git log for Relicario-* trailers. Output structured table or JSON array. +``` + +### Onboarding Flow + +1. Alice runs `relicario device add`, exports her ed25519 public key. +2. Alice sends her public key to an admin out-of-band (Signal, email, printed QR — Relicario does not mediate key exchange). +3. Admin runs `org add-member --key --name Alice`. +4. Alice pulls the org repo. She can now open the org vault. + +### Offboarding Flow + +1. Admin runs `org remove-member `. +2. Admin immediately runs `org rotate-key`. +3. Former member's wrapped key is gone; even with the old key material they cannot decrypt items written after rotation. + +### Extension — Org Context + +The vault tab grows a top-level org switcher (Personal + each configured org). Switching context loads the selected org's manifest through the service worker. The SW holds the unwrapped org master key in a `Zeroizing<[u8; 32]>` session handle — identical pattern to the personal master key. The org master key is never written to `localStorage`, `IndexedDB`, or any persistent browser storage. + +--- + +## Audit Trail + +### Git Log as Audit Record + +Every write to the org repo is a git commit. Commits use structured trailers for machine-readable audit: + +``` +add item to prod-infra collection + +Relicario-Actor: alice +Relicario-Action: item-create +Relicario-Collection: prod-infra +Relicario-Item: 9f8e7d6c5b4a3f2e +Relicario-Device: d1c2b3a4e5f6d1c2 +``` + +The trailer schema is versioned via `org.json`'s `schema_version`. + +### Action Vocabulary + +| `Relicario-Action` | Trigger | +|---|---| +| `item-create` | New item added to org vault | +| `item-update` | Existing org item edited | +| `item-delete` | Item moved to trash | +| `item-purge` | Trashed item permanently deleted | +| `member-add` | `org add-member` | +| `member-remove` | `org remove-member` | +| `member-role-change` | `org set-role` | +| `collection-create` | `org create-collection` | +| `collection-grant` | `org grant` | +| `collection-revoke` | `org revoke` | +| `key-rotate` | `org rotate-key` | + +### `relicario org audit` + +A query layer over `git log --format`. Parses trailer lines, applies filters, emits structured output. + +`--format json` emits a JSON array of event objects — pipe to any log shipper without a live webhook: + +```json +[ + { + "timestamp": 1748000000, + "actor_name": "alice", + "actor_id": "a1b2c3d4e5f6a1b2", + "action": "item-create", + "collection": "prod-infra", + "item_id": "9f8e7d6c5b4a3f2e", + "device_id": "d1c2b3a4e5f6d1c2", + "commit": "abc123def456" + } +] +``` + +A cron job running `relicario org audit --since --format json | ` is sufficient for Splunk/Elastic/Loki ingestion without any live server. + +### Read Audit Gap + +Git commits only record writes. Read access (a member decrypting an item without modifying it) is invisible to the git log. This is the same limitation as Bitwarden self-hosted's event log. Read audit requires a server that mediates item fetch — deferred to phase 2. + +--- + +## Pre-Receive Hook Policy Enforcement + +`relicario-server` validates on every push to the org repo: + +- Only owners/admins may push changes to `members.json`, `collections.json`, `org.json`. +- Members may only push item writes (`items/*.enc`) to collections in their grant list. +- `org.json` `schema_version` must not decrease. +- `members.json` and `collections.json` must pass schema validation. +- `keys/` path ACL (read-own-only) is delegated to git hosting ACLs (Gitea/Forgejo team permissions) — documented as a deployment note, not enforced by the hook. +- After `member-remove`, the hook emits a warning if `org rotate-key` has not been run since the removal but does not block further pushes. This allows an admin to batch multiple removals before a single rotation. + +--- + +## Error Handling + +### Key Rotation Race + +If two admins simultaneously remove different members and rotate the key, the second rotation's `git pull --rebase` detects a concurrent rotation commit and aborts with: `"Concurrent key rotation detected — pull and re-run org rotate-key."` Admin re-runs after pulling. + +### Org Repo Schema Invalid + +If `members.json` or `collections.json` fails schema validation on pull, the CLI refuses to open the org vault: `"Org repo schema invalid at — contact your org owner."` No silent degradation. + +### Member Device Key Lost + +If a member loses their device key before a backup device was added, an owner generates a new `keys/.enc` for a replacement device key the member generates. No master key escrow is required — owners can always re-grant access because they hold the org master key. + +### Extension Offline + +If the git remote is unreachable, the extension operates read-only from the last-pulled org repo state. Writes are blocked and the org context shows an "org offline — writes disabled" indicator. Identical behavior to personal vault offline mode. + +--- + +## Testing Strategy + +### Unit Tests (`relicario-core`) + +- Org master key wrap/unwrap round-trip: ed25519 → X25519 conversion + XChaCha20-Poly1305 encrypt/decrypt. +- Manifest filtering by collection grant list: member with grants `["prod-infra"]` receives only items tagged `prod-infra`. +- `members.json` and `collections.json` schema validation: valid and invalid inputs. + +Fast Argon2id params are irrelevant for org tests — the org crypto path bypasses Argon2id entirely (key wrapping is X25519-based). Standard test params apply to any personal vault operations in shared test fixtures. + +### Integration Tests (`relicario-cli`) + +- Full `org init` → `add-member` → `grant` → item write → audit output against a local bare git repo. +- `org remove-member` → `rotate-key` → former member cannot decrypt newly written items. +- `org audit --format json` output is valid JSON matching the action vocabulary. +- Pre-receive hook rejects a push where the committing member writes to an ungranated collection. +- Concurrent rotation race: two concurrent `rotate-key` operations, second one detects and aborts. + +### Extension Tests (vitest) + +- SW org context switching: loading org manifest replaces personal manifest in session state without cross-contamination. +- Org master key is stored only in the Zeroizing session handle, never in `localStorage` or `IndexedDB`. +- Offline read-only mode: SW surfaces read-only flag when git remote returns a network error. + +--- + +## Phase Boundary + +This spec covers phase 1 (git-native org). Phase 2 adds: + +- HTTP management plane via `relicario-server` (relay skeleton → org API) +- Live audit event streaming to SIEM (webhooks, not cron-poll) +- SSO/SAML assertion validation + LDAP/IdP member sync +- Read audit (server-mediated item fetch with event recording) +- "Hide value" autofill (per-item subkeys or server-mediated relay) +- Per-collection cryptographic isolation (subkeys — explicit non-goal for phase 1)