adlee-was-taken a332a9e80d Merge feature/v0.5.0-plan-a-security-cleanup: Plan A security + cleanup
v0.5.0 Plan A — Security Fixes + Repo Cleanup. 7 commits, ~800 net
insertions across the Rust workspace. Four items delivered:

- S1 (HIGH-severity authentication bypass fix): rewrite verify_commit
  in relicario-server. The previous implementation accepted any
  GOODSIG/Good signature line on stderr, ignoring whether the signing
  key was registered or revoked. The new implementation:
  * builds a temp gpg.ssh.allowedSignersFile from devices.json at the
    commit (no global git-config mutation)
  * parses the SHA-256 fingerprint from `git verify-commit --raw`
    stderr via regex
  * checks revocation FIRST (revoked entries may have been removed
    from devices.json), with the historical-commit case
    (committer_ts < revoked_at) explicitly allowed
  * uses committer date (GIT_COMMITTER_DATE / `git show -s
    --format=%ct`), not author date or wall clock
  * tightened the bootstrap guard to require BOTH devices and revoked
    to be empty (closes an empty-devices.json privilege-escalation
    route present in the original code)
  * 4 acceptance integration tests build real on-disk repos with
    SSH-signed commits and verify each scenario

- S2 (tar archive path-traversal hardening): replace
  tar::Archive::unpack with safe_unpack_git_archive. Located in
  relicario-core (per-spec, so integration tests can reach it without
  the bytes-in/bytes-out invariant breaking). Validates each entry's
  type (rejects symlinks/hardlinks), path components (rejects '..',
  RootDir, Windows drive Prefix), and declared size (rejects
  individual or cumulative > 100×compressed-or-1-GiB whichever is
  lower). The CLI's restore path adds a paranoid OS-level
  starts_with(.git/) check on the joined destination as
  defense-in-depth even after textual validation. 5 acceptance tests
  cover path traversal, symlinks, oversized headers (header claim of
  2 GiB tested without allocating disk).

- S3 (RELICARIO_* env-var audit): docs/SECURITY.md gains a
  "Configuration env vars" section enumerating each variable, its
  purpose, and trust assumption. Active-in-all-builds variables
  (RELICARIO_IMAGE, RELICARIO_GITEA_*) are documented; debug-only
  variables (RELICARIO_NO_GROUPS_CACHE, RELICARIO_TEST_*) are gated
  behind cfg(debug_assertions) so the env-var lookup is removed from
  --release binaries.

- C1 (stale feature branch prune): 5 merged feature branches and
  3 worktrees pruned interactively per dev report.

- Bonus: 4d02a50 fixes pre-existing clippy warnings across
  crates/relicario-{core,cli} (deref operators, Option::is_none_or
  vs map_or(true, ...), iter_mut().enumerate() patterns,
  div_ceil()) so the workspace builds clean under `-D warnings`.

Merge resolution: docs/SECURITY.md had a conflict where main's F11/F12
(Device Authentication paragraph naming relicario-server + simplified
"Device registration is optional" line) collided with Plan A's S3
section. Resolved by keeping both — F11/F12's wording for the
Device Authentication section, then Plan A's "Configuration env vars"
section appended below.

Cargo.lock regenerated. The previous committed lock was stale since
commit 8855078 (--totp-qr); cargo test on both devs' worktrees
produced identical regenerated locks. Plan A genuinely added regex +
tempfile to relicario-server (both already transitively present from
relicario-cli), so no new top-level deps; the Cargo.lock churn is
catch-up of crate-version bumps that have happened since the last
commit-of-record.

Tests: 248 cargo tests pass; extension tests unchanged (336/8 with 8
pre-existing device-auth scaffolding failures).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:54:12 -04:00

Relicario

Relicario

A git-backed, self-hostable password manager where decryption requires two independent factors: a passphrase you memorize and a reference JPEG that carries a hidden secret. Compromise of either factor alone is insufficient.

The server only ever sees opaque ciphertext. There is nothing else going on. This README is the security proof.

How it works

Your passphrase (something you know)
        +
Your reference photo (something you have)
        |
        v
   [ Argon2id KDF ]  -->  master_key  -->  [ XChaCha20-Poly1305 ]  -->  encrypted vault
        ^                                                                      |
        |                                                                      v
   Never leaves                                                          Stored in git
   your device                                                         (opaque ciphertext)

At vault creation, Relicario embeds a random 256-bit secret into a carrier JPEG using DCT steganography. This photo becomes your reference image — a second factor that lives on your devices (and optionally as a "dead drop" on social media, since it survives JPEG re-encoding and mild cropping).

To unlock the vault, you provide your passphrase and point the client at the reference image. The client extracts the hidden secret, concatenates it with your passphrase, and runs Argon2id to derive the master key. Everything else follows from there.

Security model

What the server sees

A git repository containing:

  • manifest.enc — opaque binary blob
  • items/*.enc — more opaque binary blobs
  • attachments/<item-id>/*.enc — encrypted attachment blobs
  • settings.enc — encrypted vault settings
  • .relicario/salt — a random 32-byte value (not secret)
  • .relicario/params.json — Argon2id parameters (not secret)
  • .relicario/devices.json — authorized device public keys

That's it. No plaintext. No metadata about what's inside. No keys, no passphrases, no reference images.

What an attacker needs

Scenario Has Needs Result
Server breach Encrypted vault + salt Passphrase AND image secret 256+ bits of entropy. Infeasible.
Server breach + stolen image Vault + image secret Passphrase Passphrase entropy through Argon2id. 4 diceware words = ~7 million years.
Shoulder-surfed passphrase Passphrase Image secret 256 bits. Infeasible.
Stolen device Image + vault Passphrase Argon2id brute-force. Strong passphrase = safe.

No single point of failure. The two-factor design means the passphrase alone can't decrypt the vault, and the image alone can't decrypt the vault.

Compared to

Server breach entropy KDF factors
LastPass ~40-60 bits (master password only) 1
Bitwarden ~40-60 bits (master password only) 1
1Password password + 128-bit Secret Key 2
Relicario password + 256-bit image secret 2

What we don't protect against

  • A compromised device with active malware. No software password manager can.
  • Weak passphrases with a stolen reference image. Use 4+ diceware words.
  • Rubber-hose cryptanalysis.

Quick start

# Build from source
cargo build --release

# Create a vault (pick any JPEG as the carrier)
relicario init --image vacation.jpg --output reference.jpg

# Add a credential
relicario add

# Retrieve it
relicario get github

# List everything
relicario list

# Sync with your git remote
relicario sync

# Generate a random password
relicario generate -l 32

Environment variable

Set RELICARIO_IMAGE=/path/to/reference.jpg to avoid being prompted for the image path on every command.

The reference image

The reference JPEG is generated once during relicario init. It looks like a normal photo — because it is one. The 256-bit secret is embedded in the DCT coefficients of the luminance channel using Quantization Index Modulation, with heavy redundancy and Reed-Solomon-style majority voting across multiple copies.

The embedding survives:

  • JPEG recompression (tested down to quality 85)
  • Up to ~10-15% cropping from any edge
  • Social media re-encoding (Instagram, Discord, etc.)

This means your reference image can live on your Instagram, your personal website, or anywhere else. It's useless without your passphrase.

Architecture

relicario/
├── crates/
│   ├── relicario-core/     # Platform-agnostic library (no filesystem, no network)
│   │   ├── crypto.rs    # Argon2id KDF + XChaCha20-Poly1305 AEAD
│   │   ├── imgsecret.rs # DCT steganography: embed/extract 256-bit secrets in JPEGs
│   │   ├── item.rs      # Item, Field, Manifest data model (serde)
│   │   ├── item_types/  # Per-type cores (Login, SecureNote, Card, Identity, Key, Document, Totp)
│   │   ├── attachment.rs # Encrypted attachment helpers (content-addressed)
│   │   ├── settings.rs  # VaultSettings (retention, generator defaults, caps)
│   │   ├── backup.rs    # `.relbak` encrypted-backup envelope
│   │   ├── device.rs    # ed25519 device keys + revocation entries
│   │   └── vault.rs     # Encrypt/decrypt items, manifest, settings
│   ├── relicario-cli/      # CLI binary: filesystem, git, terminal I/O
│   ├── relicario-wasm/     # Thin wasm-bindgen wrapper for the browser extension
│   └── relicario-server/   # Pre-receive hook: device-signature verification
├── extension/              # Chrome MV3 / Firefox WebExtension (TypeScript)
└── docs/
    ├── ARCHITECTURE.md    # System overview + flow diagrams
    ├── SECURITY.md        # Manifest integrity model + threat notes
    ├── architecture/      # Cross-codebase + per-codebase architecture docs
    └── superpowers/
        └── specs/         # Design specifications with full threat model

relicario-core takes bytes and returns bytes. It has no knowledge of filesystems, git, or networks. This makes it portable to WASM (browser extension), Android (JNI), and iOS (Swift bridge).

Crypto primitives

Primitive Purpose Why this one
Argon2id (64 MiB, 3 iter, 4 parallel) Key derivation from passphrase + image secret Memory-hard, GPU-resistant, OWASP recommended
XChaCha20-Poly1305 Authenticated encryption of vault entries 192-bit nonce (no collision risk), fast in WASM/ARM without AES-NI
ed25519 Device key signing Per-device commit authorization, revocable without KDF rotation

Encrypted file format

version (1 byte) | nonce (24 bytes) | ciphertext (variable) | auth tag (16 bytes)

Every write generates a fresh random nonce. The version byte allows future format changes.

Vault layout

my-vault.git/
├── manifest.enc          # Encrypted item index (names, URLs, timestamps)
├── settings.enc          # Encrypted vault settings (retention, caps, generator defaults)
├── items/
│   ├── a1b2c3d4e5f6a7b8.enc   # One encrypted item per file
│   └── …
├── attachments/
│   └── <item-id>/
│       └── <aid>.enc     # Content-addressed encrypted attachment blob
└── .relicario/
    ├── salt              # 32-byte random salt (not secret)
    ├── params.json       # KDF parameters
    ├── devices.json      # Authorized device public keys
    └── revoked.json      # Revoked device records (when device auth is enabled)

Item IDs are random 16-char hex strings (64 bits of entropy). Git history is preserved — every add/edit/delete is a commit. "When was this password last rotated?" is answered by git log and by the per-item field history.

Device management

Each device generates its own ed25519 keypair. The public key is stored in .relicario/devices.json (committed to the repo). Device keys are used for commit signing — they do NOT participate in vault decryption.

Revoking a device: remove its key from devices.json and commit. No passphrase or reference image rotation needed.

relicario device add --name laptop
relicario device list
relicario device revoke laptop

Building

Requires Rust stable (1.70+).

git clone ssh://git@git.adlee.work:2222/alee/relicario.git
cd relicario
cargo build --release
cargo test

The binary is at target/release/relicario.

Roadmap

  • WASM build + Chrome MV3 browser extension (inline crypto, no native messaging)
  • Firefox WebExtension build
  • Typed items: Login, SecureNote, Identity, Card, Key, Document, TOTP
  • Secure document storage (encrypted file attachments)
  • Backup & restore (.relbak encrypted envelope)
  • LastPass CSV import
  • Device authentication (ed25519 commit signing + pre-receive hook)
  • Import from Bitwarden / 1Password
  • relicario unlock daemon (ssh-agent-style, holds master key for a TTL)
  • Android/iOS clients (Rust core compiles to ARM)
  • Safari extension

License

MIT


Built by Aaron Lee. Design spec and threat model in docs/superpowers/specs/.

Description
Host your own cloud password manager using passphrase and reference photo (DCT) security factors.
Readme GPL-3.0 3.9 MiB
Languages
TypeScript 54.8%
Rust 37.6%
CSS 6.4%
HTML 0.5%
Shell 0.3%
Other 0.4%