# Relicario — Crypto Pipeline

> **Audience:** anyone evaluating or auditing the crypto. This doc owns Argon2id parameters and rationale, XChaCha20-Poly1305 rationale, vault creation/unlock flow diagrams, DCT-steganography embed and extract flows, and the high-level encrypted-file-format diagram. **Does NOT own:** byte-level schemas or JSON shapes (see [FORMATS.md](FORMATS.md)), attacker scenarios (see [SECURITY.md](SECURITY.md)), or per-module crypto implementation (see [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md)).

## System Overview

```
┌──────────────────────────────────────────────────────────────────┐
│                        CLIENT DEVICE (trusted)                    │
│                                                                   │
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────────────┐ │
│  │  Reference   │    │  Passphrase  │    │   relicario-cli         │ │
│  │  JPEG        │    │  (typed)     │    │   or browser ext     │ │
│  │  (on disk)   │    │              │    │                      │ │
│  └──────┬───────┘    └──────┬───────┘    └──────────┬───────────┘ │
│         │                   │                       │             │
│         ▼                   │                       │             │
│  ┌──────────────┐           │                       │             │
│  │  imgsecret   │           │                       │             │
│  │  ::extract() │           │                       │             │
│  └──────┬───────┘           │                       │             │
│         │                   │                       │             │
│         ▼                   ▼                       │             │
│  ┌──────────────────────────────┐                   │             │
│  │        Argon2id KDF          │                   │             │
│  │  password = passphrase ‖     │                   │             │
│  │           image_secret       │                   │             │
│  │  salt = vault_salt           │                   │             │
│  │  → master_key (32 bytes)     │                   │             │
│  └──────────────┬───────────────┘                   │             │
│                 │                                    │             │
│                 ▼                                    │             │
│  ┌──────────────────────────────┐                   │             │
│  │   XChaCha20-Poly1305         │◄──────────────────┘             │
│  │   encrypt / decrypt          │                                 │
│  │   (192-bit nonce, 256-bit    │                                 │
│  │    key, 128-bit auth tag)    │                                 │
│  └──────────────┬───────────────┘                                 │
│                 │                                                  │
└─────────────────┼──────────────────────────────────────────────────┘
                  │
                  │ git push / pull (HTTPS or SSH)
                  ▼
┌──────────────────────────────────────────────────────────────────┐
│                     GIT SERVER (untrusted)                        │
│                                                                   │
│  relicario-vault.git/                                             │
│  ├── manifest.enc        ← opaque ciphertext                      │
│  ├── settings.enc        ← opaque ciphertext                      │
│  ├── items/                                                       │
│  │   ├── a1b2c3d4e5f6a7b8.enc   ← opaque ciphertext               │
│  │   └── …                                                        │
│  ├── attachments/                                                 │
│  │   └── <item-id>/<aid>.enc    ← opaque ciphertext               │
│  └── .relicario/                                                  │
│      ├── salt            ← 32 bytes (not secret)                  │
│      ├── params.json     ← KDF params (not secret)                │
│      ├── devices.json    ← device public keys (not secret)        │
│      └── revoked.json    ← revoked device records (not secret)    │
│                                                                   │
│  The server sees NOTHING useful. No keys, no plaintext,           │
│  no metadata about what's inside.                                 │
└──────────────────────────────────────────────────────────────────┘
```

## Vault Creation Flow

```
User provides:                     System generates:
├── carrier JPEG (any photo)        ├── image_secret (256-bit random)
└── passphrase (memorized)          └── vault_salt (256-bit random)

                    ┌──────────────────┐
carrier JPEG ──────►│  imgsecret       │──────► reference.jpg
image_secret ──────►│  ::embed()       │        (looks like a normal photo,
                    │  DCT stego       │         carries hidden secret)
                    └──────────────────┘

                    ┌──────────────────┐
passphrase ────────►│                  │
                    │  Argon2id        │──────► master_key (32 bytes)
image_secret ──────►│  (64 MiB, 3 it) │        (held in memory only)
vault_salt ────────►│                  │
                    └──────────────────┘

                    ┌──────────────────┐
master_key ────────►│  XChaCha20-      │──────► manifest.enc
empty manifest ────►│  Poly1305        │        settings.enc
default settings ──►│  encrypt (×2)    │        (parallel artifacts;
                    └──────────────────┘         independent nonces)

                    ┌──────────────────┐
                    │  git init        │──────► vault repo
                    │  git commit      │        (ready to push)
                    └──────────────────┘
```

Item creation, the typed-item envelope (`Item` + per-type `ItemCore`),
attachment encryption, and field-history tracking are not shown above —
they are described in [`crates/relicario-core/ARCHITECTURE.md`](../crates/relicario-core/ARCHITECTURE.md).
The flow above covers only the crypto-pipeline shape that vault init
establishes; the per-item lifecycle reuses the same `master_key` +
XChaCha20-Poly1305 primitives against `items/<id>.enc` and
`attachments/<item-id>/<aid>.enc`.

## Unlock Flow (every vault operation)

```
                    ┌──────────────────┐
reference.jpg ─────►│  imgsecret       │──────► image_secret
                    │  ::extract()     │        (256 bits)
                    └──────────────────┘

                    ┌──────────────────┐
passphrase ────────►│                  │
image_secret ──────►│  Argon2id        │──────► master_key
vault_salt ────────►│                  │
                    └──────────────────┘

                    ┌──────────────────┐
master_key ────────►│  XChaCha20       │──────► plaintext entries
*.enc files ───────►│  ::decrypt()     │        (in memory only)
                    └──────────────────┘
```

## Org-key ECIES wrap/unwrap

Org vaults use a different key-derivation path than personal vaults. There is no
passphrase, no reference JPEG, and no Argon2id involved. Instead, each org has a
single random **org master key** that is wrapped per-member using X25519 ECIES and
stored as an opaque blob in `keys/<member-id>.enc` inside the org repo.

### Org master key

```
generate_org_key()                                          (org.rs:230)
  → OsRng → 256-bit random
  → Zeroizing<[u8; 32]>     (held in memory; never written in the clear)
```

One org key per org. It is re-generated on every `org rotate-key` operation.

### ed25519 → X25519 conversion

Each Relicario device holds an ed25519 signing key. To participate in ECIES the
ed25519 key pair must be mapped to X25519:

```
Recipient public key (for wrap):
  ed25519 VerifyingKey
    → .to_montgomery()                  (birational Montgomery map, ed25519_dalek)
    → X25519 PublicKey

Recipient secret key (for unwrap):
  ed25519 seed (32 bytes)
    → SHA-512(seed)[..32]               (org.rs:241–242)
    → RFC 7748 clamp:
        scalar[0]  &= 248
        scalar[31] &= 127
        scalar[31] |= 64
    → x25519_dalek::StaticSecret
```

The RFC 7748 clamp and the `to_montgomery()` birational map are the standard
construction; a pinned RFC 8032 known-answer vector is verified in the unit tests
inside `org.rs`.

### Wrap flow (one blob per member)

```
                    ┌──────────────────────────────────────┐
                    │            wrap_org_key()            │   (org.rs:265)
                    │                                      │
 org_key ──────────►│  EphemeralSecret::random (OsRng)     │
                    │  ephemeral_pk = PublicKey::from(eph) │
                    │                                      │
 recipient_pk ─────►│  DH: eph_sk.diffie_hellman(rec_pk)   │
                    │  → dh_shared (32 bytes)              │
                    │                                      │
                    │  kdf_input  = dh_shared              │
                    │            ‖ ephemeral_pk (32 B)     │   (org.rs:278–281)
                    │            ‖ recipient_pk (32 B)     │
                    │  wrap_key  = SHA-256(kdf_input)      │
                    │  (kdf_input in Zeroizing<Vec<u8>>)   │
                    │  (wrap_key  in Zeroizing<[u8;32]>)   │
                    │                                      │
                    │  encrypted = crate::crypto::encrypt  │
                    │    (wrap_key, org_key)               │
                    │  → version(1) ‖ nonce(24) ‖ ct+tag  │
                    │                                      │
                    │  output: ephemeral_pk(32)            │   (org.rs:264)
                    │        ‖ version(1)                  │
                    │        ‖ nonce(24)                   │
                    │        ‖ ciphertext + tag            │
                    └──────────────────────────────────────┘
                              │
                              ▼
                    keys/<member-id>.enc  (in org repo)
```

### Unwrap flow

```
                    ┌──────────────────────────────────────┐
                    │           unwrap_org_key()           │   (org.rs:299)
                    │                                      │
 wrapped blob ─────►│  split: ephemeral_pk(32) + rest      │
                    │                                      │
 ed25519_seed ─────►│  ed25519_seed_to_x25519_secret()     │
                    │  → recipient_sk + recipient_pk       │
                    │                                      │
                    │  DH: recipient_sk.diffie_hellman(eph)│
                    │  → dh_shared                         │
                    │                                      │
                    │  kdf_input + SHA-256 → wrap_key      │
                    │  (same domain-separated KDF as wrap) │
                    │                                      │
                    │  plaintext = crate::crypto::decrypt  │
                    │    (wrap_key, rest)                  │
                    │  → Zeroizing<[u8;32]> org_key        │
                    └──────────────────────────────────────┘
```

### Key distinction: no Argon2id

Unlike the personal vault, **org crypto bypasses Argon2id entirely**:

| | Personal vault | Org vault |
|---|---|---|
| Key origin | Argon2id(passphrase ‖ image_secret, salt) | OsRng → 256-bit random |
| Key transport | Embedded in reference JPEG (stego) | X25519 ECIES wrap blob |
| AEAD primitive | XChaCha20-Poly1305 (`crate::crypto::encrypt`) | Same primitive (delegated) |
| KDF for wrap key | Argon2id | SHA-256(DH ‖ eph_pk ‖ rec_pk) |

The inner AEAD (`crate::crypto::encrypt` / `decrypt`) is **not re-implemented** in
the org module — it is called directly, so org item blobs share the identical
`version(1) ‖ nonce(24) ‖ ct+tag` wire format (`VERSION_BYTE = 0x02`,
`crates/relicario-core/src/crypto.rs:59`).

### Zeroize discipline

All intermediates that carry key material are dropped through `Zeroizing`:

- `org_key` — `Zeroizing<[u8; 32]>` everywhere it is passed
- `kdf_input` — `Zeroizing<Vec<u8>>` (org.rs:278)
- `wrap_key` — `Zeroizing<[u8; 32]>`
- decrypt `plaintext` in `unwrap_org_key` — `Zeroizing<Vec<u8>>`

### Key rotation and re-encryption

`org rotate-key` (`crates/relicario-cli/src/commands/org.rs:332`) does more than
generate a fresh org key:

```
run_rotate_key()
  1. git pull --rebase   (detect concurrent rotation → abort if non-fast-forward)
  2. generate_org_key()  → new_org_key
  3. wrap_org_key(new_org_key, member_pk)   for every current member
     → overwrites keys/<member-id>.enc
  4. re-encrypt every items/<slug>/<id>.enc blob under new_org_key
  5. re-encrypt manifest.enc under new_org_key
  6. git add + git commit via org_git_run  (signed; Relicario-Action: key-rotate)
```

`rotate-key` pulls (`--rebase`) at the start to pick up concurrent changes and
abort on a conflicting concurrent rotation, then commits locally; it does **not**
push. Publishing the rotation to the remote is a separate step (the normal git
sync path), the same way personal-vault mutations commit locally and sync later.

Re-encryption of every item blob (step 4) is deliberate: a removed member who holds
a local clone of the repo cannot decrypt any item written after the rotation, because
those blobs are sealed under a key they never received. Without re-encryption, all
pre-rotation blobs would remain readable to the former member indefinitely.

The item-CRUD commands (`org add`/`get`/`list`/`edit`/`rm`/`restore`/`purge`) that read and write these blobs are merged and wired into `main.rs`; each operates under the org master key recovered by `unwrap_org_key`.

## imgsecret DCT Embedding

```
                         Input JPEG
                             │
                             ▼
                    ┌──────────────────┐
                    │  Decode to RGB   │
                    │  Extract Y       │
                    │  (luminance)     │
                    └────────┬─────────┘
                             │
                             ▼
              ┌──────────────────────────────┐
              │        Full Image            │
              │  ┌────────────────────────┐  │
              │  │ 15%  ┌────────────┐    │  │
              │  │margin│            │15% │  │
              │  │      │  Central   │mar-│  │
              │  │      │  70%       │gin │  │
              │  │      │  EMBEDDING │    │  │
              │  │      │  REGION    │    │  │
              │  │      └────────────┘    │  │
              │  │         15% margin     │  │
              │  └────────────────────────┘  │
              └──────────────────────────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  Divide into     │
                    │  8×8 blocks      │
                    │  Apply 2D DCT    │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  For each        │
                    │  selected block: │
                    │                  │
                    │  QIM embed bits  │
                    │  in zig-zag      │
                    │  positions 6-17  │
                    │  (mid-frequency) │
                    │                  │
                    │  Repeat secret   │
                    │  MIN_COPIES (5)  │
                    │  to 50 times,    │
                    │  by capacity     │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  Inverse DCT     │
                    │  Reconstruct RGB │
                    │  Save JPEG (Q92) │
                    └──────────────────┘
                             │
                             ▼
                    Output: reference.jpg
                    (visually identical,
                     carries 256-bit secret)
```

The redundancy count is chosen at embed time based on available DCT capacity: `num_copies = (total_blocks / BLOCKS_PER_COPY).min(50)`, with `BLOCKS_PER_COPY = 22` and a floor of `MIN_COPIES = 5` (`crates/relicario-core/src/imgsecret.rs:78,530-537`). Images that cannot fit at least 5 copies are rejected before embed. Majority voting across these copies at extract time requires ≥ 60 % confidence per bit.

## Extraction (with crop recovery)

```
Input JPEG (possibly re-encoded or cropped)
         │
         ▼
┌─────────────────────────┐
│  Try canonical alignment │──── Success ──► image_secret
│  (offset 0,0)           │
└─────────┬───────────────┘
          │ Fail
          ▼
┌─────────────────────────┐
│  Search crop offsets     │
│  dx, dy: -15% to +15%   │
│  step: 8 pixels          │
│  (~16,800 candidates)    │──── Success ──► image_secret
│                          │
│  For each: extract bits, │
│  majority vote, check    │
│  confidence ≥ 60%        │
└─────────┬───────────────┘
          │ All fail
          ▼
    ExtractionFailed error
```

## Encrypted File Format

```
┌─────────┬────────────────────────┬──────────────────┬──────────────────┐
│ version │         nonce          │    ciphertext     │    auth tag      │
│ 1 byte  │       24 bytes         │    N bytes        │    16 bytes      │
│  0x02   │ random per write       │ XChaCha20 stream  │ Poly1305 MAC     │
└─────────┴────────────────────────┴──────────────────┴──────────────────┘
```

`VERSION_BYTE = 0x02` (`crates/relicario-core/src/crypto.rs:59`). Blobs starting with any other byte are rejected with `UnsupportedFormatVersion { found, expected: 0x02 }`. The legacy `0x01` format from the pre-typed-items era is no longer supported.

## Crate Architecture

```
┌────────────────────────────────────────────────────────────┐
│                    relicario-cli                               │
│  Filesystem, git (shelling out), terminal I/O, clipboard    │
│                                                             │
│  Depends on: relicario-core, clap, anyhow, rpassword, arboard │
└──────────────────────┬─────────────────────────────────────┘
                       │ uses
                       ▼
┌────────────────────────────────────────────────────────────┐
│                    relicario-core                           │
│  Platform-agnostic: bytes in, bytes out                     │
│  No filesystem, no network, no git                          │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌─────────┐  ┌────────────┐   │
│  │ crypto   │  │ imgsecret│  │ item +  │  │ vault      │   │
│  │          │  │          │  │ types   │  │            │   │
│  │ KDF      │  │ DCT      │  │ Item    │  │ encrypt_   │   │
│  │ encrypt  │  │ embed    │  │ Manifest│  │ item()     │   │
│  │ decrypt  │  │ extract  │  │ Settings│  │ decrypt_   │   │
│  │          │  │ QIM      │  │ Backup  │  │ manifest() │   │
│  │          │  │          │  │ Device  │  │ ...        │   │
│  └──────────┘  └──────────┘  └─────────┘  └────────────┘   │
│                                                             │
│  Consumed by: relicario-cli, relicario-wasm (extension),    │
│               relicario-server (pre-receive hook).          │
│  Future: JNI/Swift wrappers for Android/iOS.                │
└────────────────────────────────────────────────────────────┘
```

## Entropy at Each Attack Scenario

```
Server breach only:          ████████████████████████████  256+ bits (infeasible)
                             passphrase + image_secret

Server + stolen image:       ████░░░░░░░░░░░░░░░░░░░░░░  ~51 bits (4 diceware words)
                             passphrase through Argon2id    ~7 million years

Shoulder-surfed passphrase:  ████████████████████████████  256 bits (infeasible)
                             image_secret

Stolen device:               ████░░░░░░░░░░░░░░░░░░░░░░  ~51 bits (4 diceware words)
                             passphrase through Argon2id    ~7 million years

Both factors compromised:    game over (same as every password manager)
```

---

**Next:** [FORMATS.md](FORMATS.md) — the byte-level wire formats.
