+
+# 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.
@@ -19,7 +23,7 @@ Your reference photo (something you have)
your device (opaque ciphertext)
```
-At vault creation, idfoto 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).
+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.
@@ -30,9 +34,9 @@ To unlock the vault, you provide your passphrase and point the client at the ref
A git repository containing:
- `manifest.enc` — opaque binary blob
- `entries/*.enc` — more opaque binary blobs
-- `.idfoto/salt` — a random 32-byte value (not secret)
-- `.idfoto/params.json` — Argon2id parameters (not secret)
-- `.idfoto/devices.json` — authorized device public keys
+- `.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.
@@ -54,7 +58,7 @@ No single point of failure. The two-factor design means the passphrase alone can
| LastPass | ~40-60 bits (master password only) | 1 |
| Bitwarden | ~40-60 bits (master password only) | 1 |
| 1Password | password + 128-bit Secret Key | 2 |
-| **idfoto** | **password + 256-bit image secret** | **2** |
+| **relicario** | **password + 256-bit image secret** | **2** |
### What we don't protect against
@@ -69,31 +73,31 @@ No single point of failure. The two-factor design means the passphrase alone can
cargo build --release
# Create a vault (pick any JPEG as the carrier)
-idfoto init --image vacation.jpg --output reference.jpg
+relicario init --image vacation.jpg --output reference.jpg
# Add a credential
-idfoto add
+relicario add
# Retrieve it
-idfoto get github
+relicario get github
# List everything
-idfoto list
+relicario list
# Sync with your git remote
-idfoto sync
+relicario sync
# Generate a random password
-idfoto generate -l 32
+relicario generate -l 32
```
### Environment variable
-Set `IDFOTO_IMAGE=/path/to/reference.jpg` to avoid being prompted for the image path on every command.
+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 `idfoto 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 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)
@@ -105,20 +109,20 @@ This means your reference image can live on your Instagram, your personal websit
## Architecture
```
-idfoto/
+relicario/
├── crates/
-│ ├── idfoto-core/ # Platform-agnostic library (no filesystem, no network)
+│ ├── 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
│ │ ├── entry.rs # Entry, Manifest data model (serde)
│ │ └── vault.rs # Encrypt/decrypt entries and manifests
-│ └── idfoto-cli/ # CLI binary: filesystem, git, terminal I/O
+│ └── relicario-cli/ # CLI binary: filesystem, git, terminal I/O
└── docs/
└── superpowers/
└── specs/ # Design specification with full threat model
```
-`idfoto-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).
+`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
@@ -144,7 +148,7 @@ my-vault.git/
├── entries/
│ ├── a1b2c3d4.enc # One encrypted entry per file
│ └── e5f6a7b8.enc
-└── .idfoto/
+└── .relicario/
├── salt # 32-byte random salt (not secret)
├── params.json # KDF parameters
└── devices.json # Authorized device public keys
@@ -154,14 +158,14 @@ Entry IDs are random hex strings. Git history is preserved — every add/edit/de
## Device management
-Each device generates its own ed25519 keypair. The public key is stored in `.idfoto/devices.json` (committed to the repo). Device keys are used for commit signing — they do NOT participate in vault decryption.
+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.
```bash
-idfoto device add --name laptop
-idfoto device list
-idfoto device revoke laptop
+relicario device add --name laptop
+relicario device list
+relicario device revoke laptop
```
## Building
@@ -169,20 +173,20 @@ idfoto device revoke laptop
Requires Rust stable (1.70+).
```bash
-git clone ssh://git@git.adlee.work:2222/alee/idfoto.git
-cd idfoto
+git clone ssh://git@git.adlee.work:2222/alee/relicario.git
+cd relicario
cargo build --release
cargo test
```
-The binary is at `target/release/idfoto`.
+The binary is at `target/release/relicario`.
## Roadmap
- [ ] WASM build + Chrome browser extension (inline crypto, no native messaging)
- [ ] Secure notes (free-form encrypted text entries)
- [ ] Secure document storage (encrypted file attachments up to 5-10 MB)
-- [ ] `idfoto unlock` daemon (ssh-agent-style, holds master key for a TTL)
+- [ ] `relicario unlock` daemon (ssh-agent-style, holds master key for a TTL)
- [ ] Android/iOS clients (Rust core compiles to ARM)
- [ ] Import from LastPass/Bitwarden/1Password
- [ ] Firefox/Safari extensions
diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml
index abc59a7..7dca4e1 100644
--- a/crates/relicario-cli/Cargo.toml
+++ b/crates/relicario-cli/Cargo.toml
@@ -2,7 +2,7 @@
name = "relicario-cli"
version = "0.1.0"
edition = "2021"
-description = "CLI for idfoto password manager"
+description = "CLI for relicario password manager"
[[bin]]
name = "relicario"
diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs
index d720710..c41ec63 100644
--- a/crates/relicario-cli/src/main.rs
+++ b/crates/relicario-cli/src/main.rs
@@ -1,4 +1,4 @@
-//! idfoto CLI -- the platform layer for the idfoto password manager.
+//! relicario CLI -- the platform layer for the relicario password manager.
//!
//! This binary provides the filesystem, git, and terminal I/O that
//! [`relicario_core`] intentionally excludes. It is the "glue" between the
@@ -69,7 +69,7 @@ struct Cli {
/// All available CLI subcommands.
#[derive(Subcommand)]
enum Commands {
- /// Initialize a new idfoto vault in the current directory.
+ /// Initialize a new relicario vault in the current directory.
/// Creates the directory structure, generates a random image secret,
/// embeds it in the carrier image, and sets up git.
Init {
@@ -150,14 +150,14 @@ struct DeviceEntry {
// ─── Helper functions ───────────────────────────────────────────────────────
/// Returns the vault root directory (the current working directory).
-/// The vault is always rooted at the directory where `idfoto` is invoked.
+/// The vault is always rooted at the directory where `relicario` is invoked.
fn vault_dir() -> PathBuf {
std::env::current_dir().expect("failed to get current directory")
}
/// Returns the path to the `.relicario/` configuration directory within the vault.
-fn idfoto_dir() -> PathBuf {
- vault_dir().join(".idfoto")
+fn relicario_dir() -> PathBuf {
+ vault_dir().join(".relicario")
}
/// Read the 32-byte vault salt from `.relicario/salt`.
@@ -166,7 +166,7 @@ fn idfoto_dir() -> PathBuf {
/// not secret (stored in plaintext) -- its purpose is to prevent precomputed
/// rainbow table attacks against the Argon2id KDF.
fn read_salt() -> Result<[u8; 32]> {
- let data = fs::read(idfoto_dir().join("salt")).context("failed to read salt")?;
+ let data = fs::read(relicario_dir().join("salt")).context("failed to read salt")?;
let mut salt = [0u8; 32];
if data.len() != 32 {
bail!("invalid salt file: expected 32 bytes, got {}", data.len());
@@ -177,7 +177,7 @@ fn read_salt() -> Result<[u8; 32]> {
/// Read the KDF parameters from `.relicario/params.json`.
fn read_params() -> Result {
- let data = fs::read_to_string(idfoto_dir().join("params.json"))
+ let data = fs::read_to_string(relicario_dir().join("params.json"))
.context("failed to read params.json")?;
let params: KdfParams = serde_json::from_str(&data).context("failed to parse params.json")?;
Ok(params)
@@ -319,7 +319,7 @@ fn generate_password(length: usize) -> String {
// ─── Command implementations ────────────────────────────────────────────────
-/// Initialize a new idfoto vault in the current directory.
+/// Initialize a new relicario vault in the current directory.
///
/// Full sequence:
/// 1. Read the carrier JPEG provided by the user.
@@ -375,18 +375,18 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
.context("failed to derive master key")?;
// 8. Create directory structure
- let idfoto = idfoto_dir();
- fs::create_dir_all(&idfoto).context("failed to create .idfoto directory")?;
+ let relicario = relicario_dir();
+ fs::create_dir_all(&relicario).context("failed to create .relicario directory")?;
fs::create_dir_all(vault_dir().join("entries")).context("failed to create entries directory")?;
// 9. Write config files
- fs::write(idfoto.join("salt"), &salt).context("failed to write salt")?;
+ fs::write(relicario.join("salt"), &salt).context("failed to write salt")?;
fs::write(
- idfoto.join("params.json"),
+ relicario.join("params.json"),
serde_json::to_string_pretty(¶ms)?,
)
.context("failed to write params.json")?;
- fs::write(idfoto.join("devices.json"), "[]").context("failed to write devices.json")?;
+ fs::write(relicario.join("devices.json"), "[]").context("failed to write devices.json")?;
// 10. Encrypt empty manifest
let manifest = Manifest::new();
@@ -404,7 +404,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
if !status.success() {
bail!("git init failed");
}
- git_commit("feat: initialize idfoto vault")?;
+ git_commit("feat: initialize relicario vault")?;
// 13. Success
eprintln!("Vault initialized successfully.");
@@ -760,7 +760,7 @@ fn cmd_sync() -> Result<()> {
/// Read the device registry from `.relicario/devices.json`.
fn read_devices() -> Result> {
- let path = idfoto_dir().join("devices.json");
+ let path = relicario_dir().join("devices.json");
let data = fs::read_to_string(&path).context("failed to read devices.json")?;
let devices: Vec = serde_json::from_str(&data).context("failed to parse devices.json")?;
Ok(devices)
@@ -769,13 +769,13 @@ fn read_devices() -> Result> {
/// Write the device registry to `.relicario/devices.json`.
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
let data = serde_json::to_string_pretty(devices)?;
- fs::write(idfoto_dir().join("devices.json"), data).context("failed to write devices.json")?;
+ fs::write(relicario_dir().join("devices.json"), data).context("failed to write devices.json")?;
Ok(())
}
/// Register a new device by generating an ed25519 keypair.
///
-/// The private key is saved to `~/.config/idfoto/.key` with
+/// The private key is saved to `~/.config/relicario/.key` with
/// restrictive permissions (0600 on Unix). The public key is added to
/// the vault's devices.json and committed to git.
///
diff --git a/crates/relicario-core/Cargo.toml b/crates/relicario-core/Cargo.toml
index 0e3b3c7..e483ac4 100644
--- a/crates/relicario-core/Cargo.toml
+++ b/crates/relicario-core/Cargo.toml
@@ -2,7 +2,7 @@
name = "relicario-core"
version = "0.1.0"
edition = "2021"
-description = "Core library for idfoto password manager"
+description = "Core library for relicario password manager"
[dependencies]
thiserror = "2"
@@ -25,3 +25,4 @@ hex = "0.4"
url = { version = "2", features = ["serde"] }
getrandom = "0.2"
+[dev-dependencies]
diff --git a/crates/relicario-core/src/crypto.rs b/crates/relicario-core/src/crypto.rs
index 8c38d78..c895ffb 100644
--- a/crates/relicario-core/src/crypto.rs
+++ b/crates/relicario-core/src/crypto.rs
@@ -298,7 +298,7 @@ mod tests {
#[test]
fn encrypt_decrypt_round_trip() {
let key = [0xABu8; 32];
- let plaintext = b"hello, idfoto!";
+ let plaintext = b"hello, relicario!";
let ciphertext = encrypt(&key, plaintext).unwrap();
let decrypted = decrypt(&key, &ciphertext).unwrap();
diff --git a/crates/relicario-core/src/error.rs b/crates/relicario-core/src/error.rs
index 1bf9cbd..c5b1f8c 100644
--- a/crates/relicario-core/src/error.rs
+++ b/crates/relicario-core/src/error.rs
@@ -14,9 +14,14 @@ use thiserror::Error;
/// steganography -> serialization -> device keys.
#[derive(Debug, Error)]
pub enum RelicarioError {
+ /// The Argon2id key derivation failed. This typically means invalid KDF
+ /// parameters were supplied (e.g., memory cost below Argon2's minimum).
#[error("key derivation failed: {0}")]
Kdf(String),
+ /// XChaCha20-Poly1305 encryption failed. In practice this is extremely rare
+ /// -- the only realistic cause is an internal library error, since the cipher
+ /// accepts arbitrary-length plaintext.
#[error("encryption failed: {0}")]
Encrypt(String),
@@ -24,6 +29,10 @@ pub enum RelicarioError {
#[error("decryption failed")]
Decrypt,
+ /// The binary ciphertext blob does not match the expected format (e.g.,
+ /// too short to contain the version byte + nonce + tag, or an unrecognized
+ /// version byte). This usually indicates file corruption or a version
+ /// mismatch between the writer and reader.
#[error("invalid vault format: {0}")]
Format(String),
@@ -42,9 +51,15 @@ pub enum RelicarioError {
#[error("attachment too large: {size} bytes > {max} bytes max")]
AttachmentTooLarge { size: u64, max: u64 },
+ /// A general error from the image steganography subsystem (imgsecret).
+ /// Covers issues like failing to decode the carrier JPEG or failing to
+ /// encode the output JPEG after modification.
#[error("imgsecret: {0}")]
ImgSecret(String),
+ /// The carrier image is too small to hold the embedded secret with
+ /// sufficient redundancy. The embed region (central 70% of the image)
+ /// must contain at least `BLOCKS_PER_COPY * MIN_COPIES` 8x8 blocks.
#[error("image too small: need at least {min_width}x{min_height}, got {actual_width}x{actual_height}")]
ImageTooSmall {
min_width: u32,
@@ -53,12 +68,22 @@ pub enum RelicarioError {
actual_height: u32,
},
+ /// Secret extraction from a JPEG failed. This can mean:
+ /// - The image never had a secret embedded in it.
+ /// - The image was recompressed below Q85, destroying the QIM watermarks.
+ /// - The image was cropped beyond the 15% crumple zone.
+ /// - Majority-vote confidence fell below the 60% threshold on one or more bits.
#[error("extraction failed: no valid secret found in image")]
ExtractionFailed,
+ /// JSON serialization or deserialization of an entry or manifest failed.
+ /// Wraps [`serde_json::Error`] transparently via `#[from]`.
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
+ /// An error related to device ed25519 key operations. Device keys are
+ /// separate from the vault KDF -- revoking a device does not require
+ /// rotating the passphrase or reference image.
#[error("device key error: {0}")]
DeviceKey(String),
}
diff --git a/crates/relicario-core/src/imgsecret.rs b/crates/relicario-core/src/imgsecret.rs
index cfd1686..d512036 100644
--- a/crates/relicario-core/src/imgsecret.rs
+++ b/crates/relicario-core/src/imgsecret.rs
@@ -1,6 +1,6 @@
//! DCT-based steganographic embedding of a 256-bit secret in JPEG images.
//!
-//! This is the novel component of idfoto. It hides a 32-byte secret inside a
+//! This is the novel component of relicario. It hides a 32-byte secret inside a
//! JPEG image's luminance channel using Quantization Index Modulation (QIM) on
//! mid-frequency DCT coefficients, with majority voting across multiple redundant
//! copies for robustness.
diff --git a/crates/relicario-core/src/lib.rs b/crates/relicario-core/src/lib.rs
index 634afa8..4bc41fd 100644
--- a/crates/relicario-core/src/lib.rs
+++ b/crates/relicario-core/src/lib.rs
@@ -1,6 +1,6 @@
//! # relicario-core
//!
-//! Platform-agnostic core library for the idfoto password manager.
+//! Platform-agnostic core library for the relicario password manager.
//!
//! This crate is intentionally **bytes-in/bytes-out** -- it performs no filesystem
//! access, no network I/O, and no git operations. All inputs arrive as byte slices
diff --git a/crates/relicario-wasm/Cargo.toml b/crates/relicario-wasm/Cargo.toml
index e82da03..0cf6eb9 100644
--- a/crates/relicario-wasm/Cargo.toml
+++ b/crates/relicario-wasm/Cargo.toml
@@ -2,7 +2,7 @@
name = "relicario-wasm"
version = "0.1.0"
edition = "2021"
-description = "WASM bindings for idfoto password manager"
+description = "WASM bindings for relicario password manager"
[lib]
crate-type = ["cdylib", "rlib"]
diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs
index 2f791a3..8a6b767 100644
--- a/crates/relicario-wasm/src/lib.rs
+++ b/crates/relicario-wasm/src/lib.rs
@@ -1,4 +1,4 @@
-//! WASM bindings for the idfoto password manager.
+//! WASM bindings for the relicario password manager.
//!
//! This crate wraps [`relicario_core`] for use in a Chrome MV3 browser extension via
//! `wasm-bindgen`. Every function marked `#[wasm_bindgen]` is callable from
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index ccafee0..f033b4d 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,4 +1,4 @@
-# idfoto — Architecture
+# relicario — Architecture
## System Overview
@@ -7,7 +7,7 @@
│ CLIENT DEVICE (trusted) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
-│ │ Reference │ │ Passphrase │ │ idfoto-cli │ │
+│ │ Reference │ │ Passphrase │ │ relicario-cli │ │
│ │ JPEG │ │ (typed) │ │ or browser ext │ │
│ │ (on disk) │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
@@ -42,12 +42,12 @@
┌──────────────────────────────────────────────────────────────────┐
│ GIT SERVER (untrusted) │
│ │
-│ idfoto-vault.git/ │
+│ relicario-vault.git/ │
│ ├── manifest.enc ← opaque ciphertext │
│ ├── entries/ │
│ │ ├── a1b2c3d4.enc ← opaque ciphertext │
│ │ └── e5f6a7b8.enc ← opaque ciphertext │
-│ └── .idfoto/ │
+│ └── .relicario/ │
│ ├── salt ← 32 bytes (not secret) │
│ ├── params.json ← KDF params (not secret) │
│ └── devices.json ← device public keys (not secret) │
@@ -209,15 +209,15 @@ Input JPEG (possibly re-encoded or cropped)
```
┌────────────────────────────────────────────────────────────┐
-│ idfoto-cli │
+│ relicario-cli │
│ Filesystem, git (shelling out), terminal I/O, clipboard │
│ │
-│ Depends on: idfoto-core, clap, anyhow, rpassword, arboard │
+│ Depends on: relicario-core, clap, anyhow, rpassword, arboard │
└──────────────────────┬─────────────────────────────────────┘
│ uses
▼
┌────────────────────────────────────────────────────────────┐
-│ idfoto-core │
+│ relicario-core │
│ Platform-agnostic: bytes in, bytes out │
│ No filesystem, no network, no git │
│ │
@@ -230,7 +230,7 @@ Input JPEG (possibly re-encoded or cropped)
│ │ │ │ QIM │ │ │ │ manifest() │ │
│ └──────────┘ └──────────┘ └─────────┘ └────────────┘ │
│ │
-│ Future: idfoto-wasm wraps this for browser extension │
+│ Future: relicario-wasm wraps this for browser extension │
│ Future: JNI/Swift wrappers for Android/iOS │
└────────────────────────────────────────────────────────────┘
```
diff --git a/docs/superpowers/audits/2026-04-18-initial-security-audit.md b/docs/superpowers/audits/2026-04-18-initial-security-audit.md
index 6374556..074f583 100644
--- a/docs/superpowers/audits/2026-04-18-initial-security-audit.md
+++ b/docs/superpowers/audits/2026-04-18-initial-security-audit.md
@@ -1,7 +1,7 @@
-# idfoto Security Audit Report
+# relicario Security Audit Report
**Date:** 2026-04-18
-**Scope:** Full static review of `crates/idfoto-core/`, `crates/idfoto-cli/`, `crates/idfoto-wasm/`, `extension/src/`, both manifests, both webpack configs, and the design spec at `docs/superpowers/specs/2026-04-11-idfoto-design.md`.
+**Scope:** Full static review of `crates/relicario-core/`, `crates/relicario-cli/`, `crates/relicario-wasm/`, `extension/src/`, both manifests, both webpack configs, and the design spec at `docs/superpowers/specs/2026-04-11-relicario-design.md`.
**Methodology:** Static review against the project's documented threat model.
---
@@ -26,7 +26,7 @@ This breaks the second of the four security invariants in the design spec ("Two-
**Remediation:**
-1. Remove `setup.html`, `setup.js`, `idfoto_wasm.js`, and `idfoto_wasm_bg.wasm` from `web_accessible_resources` entirely. The setup page is opened with `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` from the popup (`setup-wizard.ts:28`), which works fine without `web_accessible_resources` for own-origin tabs.
+1. Remove `setup.html`, `setup.js`, `relicario_wasm.js`, and `relicario_wasm_bg.wasm` from `web_accessible_resources` entirely. The setup page is opened with `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` from the popup (`setup-wizard.ts:28`), which works fine without `web_accessible_resources` for own-origin tabs.
2. In the `save_setup` handler, validate the sender: require `sender.id === chrome.runtime.id` AND `sender.url?.startsWith(chrome.runtime.getURL('setup.html'))`. Reject all other senders.
3. If a vault is already configured, require an explicit user confirmation in the popup before overwriting — don't silently swap the binding.
4. Consider hashing the (config, imageBase64) tuple and surfacing a fingerprint to the user so a swap is at least visible.
@@ -63,12 +63,12 @@ a) `escapeForHtml` uses the `div.textContent` round-trip trick. That escapes `&`
The textContent round-trip *does* escape `<`, `>`, and `&`, so injection of raw `` tags is blocked. But:
-b) The DOM the script is constructing lives in the **page's** document, not the extension's. Even if the escape were perfect, the page's existing CSS/JS sees the prompt and can read its DOM (`#idfoto-capture-prompt`, `#idfoto-save-btn`, etc.). Page JS can:
+b) The DOM the script is constructing lives in the **page's** document, not the extension's. Even if the escape were perfect, the page's existing CSS/JS sees the prompt and can read its DOM (`#relicario-capture-prompt`, `#relicario-save-btn`, etc.). Page JS can:
- Wait for the prompt to appear via `MutationObserver`, read the `` text to learn the username being saved.
- - Programmatically `.click()` `#idfoto-save-btn` to silently save attacker-substituted credentials to the user's vault. (The `Save` handler reads `username` and `password` from variables captured at `showPrompt` call time, so it'll save *correct* values — but the page can replace the button's click listener via `cloneNode`/`replaceWith` or wrap it.)
- - Programmatically `.click()` `#idfoto-never-btn` to suppress capture for the user's *real* sites by getting them blacklisted via a confusable hostname.
+ - Programmatically `.click()` `#relicario-save-btn` to silently save attacker-substituted credentials to the user's vault. (The `Save` handler reads `username` and `password` from variables captured at `showPrompt` call time, so it'll save *correct* values — but the page can replace the button's click listener via `cloneNode`/`replaceWith` or wrap it.)
+ - Programmatically `.click()` `#relicario-never-btn` to suppress capture for the user's *real* sites by getting them blacklisted via a confusable hostname.
-c) The injected button uses `id="idfoto-save-btn"`. If the page has its own element with the same id, document.getElementById on subsequent saves returns whichever the browser returns first — generally the page's. Use a Shadow DOM or unique random ids per-prompt instead.
+c) The injected button uses `id="relicario-save-btn"`. If the page has its own element with the same id, document.getElementById on subsequent saves returns whichever the browser returns first — generally the page's. Use a Shadow DOM or unique random ids per-prompt instead.
**Why it matters:** The capture flow is the easiest path to silent credential exfiltration. A malicious site can craft inputs and DOM such that submitting *any* form on the page causes the user's vault to capture and save attacker-chosen credentials labeled as the user's bank/email, or such that legitimate save prompts get `Never`-clicked and silently blacklisted.
@@ -77,7 +77,7 @@ c) The injected button uses `id="idfoto-save-btn"`. If the page has its own elem
1. Render the prompt inside a closed Shadow DOM: `const root = container.attachShadow({ mode: 'closed' });` then `root.innerHTML = ...`. Closed shadow DOM is invisible to the page's JS.
2. Replace `escapeForHtml(displayUser)` with `textContent` assignments rather than `innerHTML`. Construct the DOM with `document.createElement` + `.textContent =` for any attacker-derived strings.
3. Treat all values from `findUsernameValue` as fully untrusted; sanity-check they're not control characters or exceptionally long.
-4. Do not use stable IDs (`idfoto-save-btn`) on elements injected into a hostile DOM.
+4. Do not use stable IDs (`relicario-save-btn`) on elements injected into a hostile DOM.
---
@@ -93,7 +93,7 @@ c) The injected button uses `id="idfoto-save-btn"`. If the page has its own elem
The icon-click flow is presented as the "intended" path, but nothing in the code enforces that the icon must be the trigger. The design spec section "Autofill anti-phishing (origin checks)" is referenced in the audit prompt but is not implemented anywhere.
-**Why it matters:** This is the classic phishing primitive a password manager exists to prevent. idfoto currently has weaker origin discipline than even a manually-typed-in form would have.
+**Why it matters:** This is the classic phishing primitive a password manager exists to prevent. relicario currently has weaker origin discipline than even a manually-typed-in form would have.
**Remediation:**
@@ -108,7 +108,7 @@ The icon-click flow is presented as the "intended" path, but nothing in the code
### H1. Argon2id password input is the unprefixed concatenation of passphrase || image_secret — collision-engineerable second-preimage path
-**File:** `crates/idfoto-core/src/crypto.rs:225-227`.
+**File:** `crates/relicario-core/src/crypto.rs:225-227`.
**Issue:** `password = passphrase || image_secret`. Two distinct (passphrase, image_secret) pairs produce the same Argon2id input — e.g. `("abc", [0x44, 0x55, …])` and `("abcD", [0x55, …])` differ only in where the boundary sits but produce identical concatenations and therefore identical master keys. The design spec explicitly calls this out as "the canonical Argon2id API — no custom construction" but it's not canonical at all; concatenating two variable-length values without a length prefix is a textbook construction smell.
@@ -131,9 +131,9 @@ Cite spec line: the spec at "Key derivation" explicitly says "concatenated, 32-b
### H2. Master key never zeroized; `Vec` from `derive_master_key` and intermediate buffers leak into reallocated heap
-**File:** `crates/idfoto-core/src/crypto.rs:205-235`, `crates/idfoto-cli/src/main.rs:204-218` and every command that calls `unlock`.
+**File:** `crates/relicario-core/src/crypto.rs:205-235`, `crates/relicario-cli/src/main.rs:204-218` and every command that calls `unlock`.
-**Issue:** The Argon2id output (`output: [u8; 32]`) is returned by value, copied into an owned `Vec` in `idfoto-wasm`'s `derive_master_key` (`lib.rs:62`), then handed to JS as a `Uint8Array` whose backing memory lives in the WASM linear memory. Nothing implements `Drop` to wipe the bytes. The intermediate `password` Vec at `crypto.rs:225-227` (which contains the *passphrase plaintext* alongside the image_secret) is also dropped without zeroizing — its buffer is freed and may be reallocated for unrelated purposes, retaining the passphrase in process memory until overwritten.
+**Issue:** The Argon2id output (`output: [u8; 32]`) is returned by value, copied into an owned `Vec` in `relicario-wasm`'s `derive_master_key` (`lib.rs:62`), then handed to JS as a `Uint8Array` whose backing memory lives in the WASM linear memory. Nothing implements `Drop` to wipe the bytes. The intermediate `password` Vec at `crypto.rs:225-227` (which contains the *passphrase plaintext* alongside the image_secret) is also dropped without zeroizing — its buffer is freed and may be reallocated for unrelated purposes, retaining the passphrase in process memory until overwritten.
In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an owned `String`) is also not zeroized. The `master_key: [u8; 32]` returned from `unlock` is just a stack array — better — but it gets passed by reference to `encrypt_entry` etc. which call into XChaCha20Poly1305 internals that may copy the key.
@@ -141,7 +141,7 @@ In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an o
**Remediation:**
-1. Add `zeroize = "1"` and `zeroize_derive` to `idfoto-core`.
+1. Add `zeroize = "1"` and `zeroize_derive` to `relicario-core`.
2. Wrap `master_key` in `Zeroizing<[u8; 32]>` in both `derive_master_key` return and at all CLI/WASM call sites.
3. Wrap the temporary `password` Vec in `Zeroizing>` so its contents are wiped on drop.
4. In the CLI, zeroize the passphrase string immediately after passing into `derive_master_key`.
@@ -152,7 +152,7 @@ In the CLI, the passphrase string from `rpassword::prompt_password_stderr` (an o
### H3. Passphrase strength gate is purely cosmetic; the only enforced minimum is 8 characters
-**File:** `crates/idfoto-cli/src/main.rs:354-356`, `extension/src/setup/setup.ts:74-85, 363-373`.
+**File:** `crates/relicario-cli/src/main.rs:354-356`, `extension/src/setup/setup.ts:74-85, 363-373`.
**Issue:** The CLI requires `>= 8` characters — no entropy enforcement. The extension calls `passphraseStrength()` purely for the colored bar; the create-vault step accepts any non-empty passphrase including a single character (`if (!state.passphrase) bail`). This contradicts the spec's "Adversaries → Stolen device + weak passphrase: enforce minimum passphrase strength at vault creation" defense.
@@ -170,7 +170,7 @@ The threat model says the passphrase carries the entire entropy load against an
### H4. CLI git_commit shells out without disabling pager / signed commits / hooks; no git config isolation
-**File:** `crates/idfoto-cli/src/main.rs:239-257, 402-405, 736-756`.
+**File:** `crates/relicario-cli/src/main.rs:239-257, 402-405, 736-756`.
**Issue:** Every CLI mutation runs `git add -A` then `git commit -m `. There are no environmental guards:
@@ -189,13 +189,13 @@ Command::new("git")
"-c", "core.editor=true", "commit", "-m", message])
```
-Stage only the specific files the operation touched (`entries/.enc`, `manifest.enc`, `.idfoto/devices.json`) instead of `git add -A`.
+Stage only the specific files the operation touched (`entries/.enc`, `manifest.enc`, `.relicario/devices.json`) instead of `git add -A`.
---
### H5. WASM `generate_password` uses `Math.random()` — claimed "non-security-critical" is wrong
-**File:** `crates/idfoto-wasm/src/lib.rs:240-256`.
+**File:** `crates/relicario-wasm/src/lib.rs:240-256`.
**Issue:** The doc comment says "Uses `js_sys::Math::random()` for randomness (not cryptographically secure, but sufficient for password character selection)." This is **flatly wrong**. Generated passwords are the user's stored credential for whatever site they're saving — they must be CSPRNG-derived. `Math.random()` is V8's xorshift128+ which is:
@@ -205,7 +205,7 @@ Stage only the specific files the operation touched (`entries/.enc`, `manife
The ext-bundled `crypto.getRandomValues` is available in service-worker context (it's used at `setup.ts:384`). There is no reason to use `Math.random` here.
-**Remediation:** Replace both `generate_password` and `generate_entry_id` in `idfoto-wasm` to use `getrandom` (already in the dependency list with `features = ["js"]` enabled, line in `Cargo.toml`). Equivalent to:
+**Remediation:** Replace both `generate_password` and `generate_entry_id` in `relicario-wasm` to use `getrandom` (already in the dependency list with `features = ["js"]` enabled, line in `Cargo.toml`). Equivalent to:
```rust
use rand::{rngs::OsRng, RngCore};
@@ -221,7 +221,7 @@ Also: the modulo-by-charset-length introduces small bias (`CHARSET.len() = 87`,
### H6. CLI password generator has modulo bias
-**File:** `crates/idfoto-cli/src/main.rs:308-317`.
+**File:** `crates/relicario-cli/src/main.rs:308-317`.
**Issue:** `(rng.next_u32() as usize) % CHARSET.len()` where `CHARSET.len() == 75`. Since `2^32 % 75 = 1` (≈), bias is mild, but still nonzero. For a tool whose entire job is generating high-entropy secrets, use `rand::distributions::Uniform` or rejection sampling.
@@ -236,7 +236,7 @@ let dist = Uniform::from(0..CHARSET.len());
### H7. `rpassword 5.0.1` is from 2020 and the API used (`prompt_password_stderr`) was deprecated and removed in 6.x
-**File:** `crates/idfoto-cli/Cargo.toml` (`rpassword = "5"`), `main.rs:205, 352, 358`.
+**File:** `crates/relicario-cli/Cargo.toml` (`rpassword = "5"`), `main.rs:205, 352, 358`.
**Issue:** `rpassword 5.0.1` predates several documented platform handling fixes (Windows console, terminal-restoration on signal). The current crate is at 7.x. `prompt_password_stderr` was removed; use `prompt_password` and pipe it to stderr separately, or call `rpassword::prompt_password_from_bufread` for testability. Stale dep is a supply-chain hygiene issue and may carry unfixed terminal-restoration bugs that leave the TTY in no-echo mode if the user Ctrl-C's mid-prompt.
@@ -266,19 +266,19 @@ The spec says this is "acceptable" and that the reference image is supposed to l
### M1. `read_block` panics on out-of-bounds via `read_block_abs(...).unwrap()`
-`crates/idfoto-core/src/imgsecret.rs:252-256`. Future block-selection changes could panic at runtime; in WASM this aborts the whole service worker. Return `Result` and propagate, or `debug_assert!`.
+`crates/relicario-core/src/imgsecret.rs:252-256`. Future block-selection changes could panic at runtime; in WASM this aborts the whole service worker. Return `Result` and propagate, or `debug_assert!`.
### M2. `bits_to_bytes` length not validated in `try_extract_with_layout`
-`crates/idfoto-core/src/imgsecret.rs:765-768`. `secret.copy_from_slice(&result_bytes[..32])` panics if `result_bytes.len() < 32`. Add `debug_assert_eq!` and prefer `try_into()`.
+`crates/relicario-core/src/imgsecret.rs:765-768`. `secret.copy_from_slice(&result_bytes[..32])` panics if `result_bytes.len() < 32`. Add `debug_assert_eq!` and prefer `try_into()`.
### M3. `extract_with_crop_recovery` has unbounded compute for attacker-controlled JPEG dimensions
-`crates/idfoto-core/src/imgsecret.rs:784-833`. A 32000×32000 attacker-supplied JPEG can wedge the service worker for tens of seconds. Cap `MAX_DIMENSION` (e.g. 10000 px) and peek dimensions before full decode.
+`crates/relicario-core/src/imgsecret.rs:784-833`. A 32000×32000 attacker-supplied JPEG can wedge the service worker for tens of seconds. Cap `MAX_DIMENSION` (e.g. 10000 px) and peek dimensions before full decode.
### M4. `decrypt` error path leaks coarse timing about which validation failed first
-`crates/idfoto-core/src/crypto.rs:115-141`. Not exploitable today (only attacker-supplied ciphertexts are the user's own files). If a "share an entry" feature lands, this becomes a side channel. Consider returning `IdfotoError::Decrypt` for all failure modes.
+`crates/relicario-core/src/crypto.rs:115-141`. Not exploitable today (only attacker-supplied ciphertexts are the user's own files). If a "share an entry" feature lands, this becomes a side channel. Consider returning `RelicarioError::Decrypt` for all failure modes.
### M5. `chrome.tabs.sendMessage` in fill_credentials sends to currently-active tab without verifying the tab matches the entry's origin
@@ -286,19 +286,19 @@ The spec says this is "acceptable" and that the reference image is supposed to l
### M6. CLI clipboard clear is best-effort and racy
-`crates/idfoto-cli/src/main.rs:565-585`. The 30s clear thread holds a *clone* of the plaintext password for 30 seconds and won't clear if user copies anything else and back. Always clear unconditionally; wrap in `Zeroizing`.
+`crates/relicario-cli/src/main.rs:565-585`. The 30s clear thread holds a *clone* of the plaintext password for 30 seconds and won't clear if user copies anything else and back. Always clear unconditionally; wrap in `Zeroizing`.
### M7. CLI prints the full password to stdout via `println!`
-`crates/idfoto-cli/src/main.rs:553`. `idfoto get` prints `"Password: "` to stdout — ends up in scrollback, `script` transcripts, tmux capture, pipes. Show `********` by default; require `--show` flag.
+`crates/relicario-cli/src/main.rs:553`. `relicario get` prints `"Password: "` to stdout — ends up in scrollback, `script` transcripts, tmux capture, pipes. Show `********` by default; require `--show` flag.
### M8. CLI generates entry IDs with only 32 bits of randomness; 8-char hex collisions are realistic
-`crates/idfoto-core/src/entry.rs:159-163`. Birthday-bound: ~65k entries gives ~50% collision; `manifest.add_entry` silently overwrites. Bump to 16-char hex (64 bits), or check before write.
+`crates/relicario-core/src/entry.rs:159-163`. Birthday-bound: ~65k entries gives ~50% collision; `manifest.add_entry` silently overwrites. Bump to 16-char hex (64 bits), or check before write.
### M9. WASM TOTP code has no guard against `result[offset + 3]` index when HMAC output is exactly 20 bytes
-`crates/idfoto-wasm/src/lib.rs:227-232`. Safe today (HMAC-SHA1 is always 20 bytes, max offset is 15). Add `debug_assert_eq!(result.len(), 20)` for future-proofing.
+`crates/relicario-wasm/src/lib.rs:227-232`. Safe today (HMAC-SHA1 is always 20 bytes, max offset is 15). Add `debug_assert_eq!(result.len(), 20)` for future-proofing.
### M10. `setup-wizard.ts` opens a new tab, but `window.close()` is no-op if popup is not in popup context
@@ -306,24 +306,24 @@ The spec says this is "acceptable" and that the reference image is supposed to l
### M11. CLI `now_iso8601` returns Unix seconds but the field is named `iso8601` and the spec promises ISO 8601 formatting
-`crates/idfoto-cli/src/main.rs:263-268`. Function name lies; consumers may parse timestamps and silently mishandle a numeric value. Either rename or use chrono/jiff.
+`crates/relicario-cli/src/main.rs:263-268`. Function name lies; consumers may parse timestamps and silently mishandle a numeric value. Either rename or use chrono/jiff.
### M12. `arboard 3` carries platform-dependent behavior; password may persist after `set_text("")` on Linux X11
-`crates/idfoto-cli/src/main.rs:572-579`. Document Linux limitations.
+`crates/relicario-cli/src/main.rs:572-579`. Document Linux limitations.
---
## LOW / INFORMATIONAL
-- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/idfoto-core/src/imgsecret.rs:163, 166`).
-- **L2.** `IdfotoError::Format` exposes the offending version byte in user-facing error string. Minor info disclosure.
+- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/relicario-core/src/imgsecret.rs:163, 166`).
+- **L2.** `RelicarioError::Format` exposes the offending version byte in user-facing error string. Minor info disclosure.
- **L3.** Capture flow's `check_credential` decrypts every candidate entry on every form submit (`index.ts:421-423`). Cache password hash, not password.
- **L4.** `popup.ts:16-20` `setState` triggers full re-render every state change — in-flight async responses can race and double-fire.
- **L5.** Chrome MV3 manifest CSP includes `'wasm-unsafe-eval'` — required but document why.
- **L6.** `git-host.ts:27` uses `String.fromCharCode(bytes[i])` for base64 — vulnerable to memory pressure with large reference images. Use chunked or `FileReader`.
- **L7.** `Cargo.toml` allows wide major-version ranges. No `cargo audit` / `cargo deny` config in repo.
-- **L8.** CLI `vault_dir()` silently returns `current_dir()` — `idfoto add` in `/home` will start writing files there. Detect missing `.idfoto/` and bail.
+- **L8.** CLI `vault_dir()` silently returns `current_dir()` — `relicario add` in `/home` will start writing files there. Detect missing `.relicario/` and bail.
- **L9.** `devices.json` initial write differs between CLI (`"[]"`) and extension (`'{"devices":[]}'`). Schema mismatch.
- **L10.** `totpSecretCache` (`Map` of plaintext base32 secrets) has no zeroization — note that JS strings can't be zeroized.
- **L11.** `escapeHtml` at `popup.ts:16-20` doesn't escape `'` (single quote). Codebase uses double quotes for attributes, so currently safe but fragile.
@@ -341,7 +341,7 @@ These primitives and parameters are correctly used and do **not** need further w
4. **`crypto.getRandomValues`** in setup wizard for image_secret + salt (`setup.ts:383-393`).
5. **ed25519-dalek 2.2.0** with `rand_core` — modern strict-verification version.
6. **TOTP / RFC 6238** in WASM is correct; unit tests exercise published RFC test vectors (`wasm/lib.rs:280-301`).
-7. **AEAD failure → opaque `IdfotoError::Decrypt`** with generic message ("wrong key or corrupted data"). Avoids leaking which factor is wrong (`error.rs:33`, `crypto.rs:138`).
+7. **AEAD failure → opaque `RelicarioError::Decrypt`** with generic message ("wrong key or corrupted data"). Avoids leaking which factor is wrong (`error.rs:33`, `crypto.rs:138`).
8. **Version byte (0x01)** at start of every ciphertext blob with rejection of unknown versions.
9. **Two-factor independence** verified by `tests/integration.rs:120-153`.
10. **DCT round-trip correctness** verified to 1e-6 tolerance.
@@ -367,6 +367,6 @@ These primitives and parameters are correctly used and do **not** need further w
## Summary
-idfoto's *core cryptography* is solid: correct AEAD, correct KDF parameters, real two-factor key derivation. The bugs are concentrated in the *extension boundary* and the *plumbing around the crypto*: the setup wizard is web-accessible without sender checks (C1), the message router trusts every caller (C2), capture and autofill have no origin discipline (C3, C4), the WASM password generator is non-cryptographic (H5), and master-key/passphrase memory hygiene is absent (H2).
+relicario's *core cryptography* is solid: correct AEAD, correct KDF parameters, real two-factor key derivation. The bugs are concentrated in the *extension boundary* and the *plumbing around the crypto*: the setup wizard is web-accessible without sender checks (C1), the message router trusts every caller (C2), capture and autofill have no origin discipline (C3, C4), the WASM password generator is non-cryptographic (H5), and master-key/passphrase memory hygiene is absent (H2).
**C1–C4 together are exploitable end-to-end and should be treated as release blockers.** H1–H8 should land before any tagged 1.0; M-class items can be batched into hardening PRs.
diff --git a/docs/superpowers/plans/2026-04-11-idfoto-core-cli.md b/docs/superpowers/plans/2026-04-11-relicario-core-cli.md
similarity index 91%
rename from docs/superpowers/plans/2026-04-11-idfoto-core-cli.md
rename to docs/superpowers/plans/2026-04-11-relicario-core-cli.md
index 6e789cd..7438912 100644
--- a/docs/superpowers/plans/2026-04-11-idfoto-core-cli.md
+++ b/docs/superpowers/plans/2026-04-11-relicario-core-cli.md
@@ -1,46 +1,46 @@
-# idfoto Core + CLI Implementation Plan
+# relicario Core + CLI Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a working git-backed password manager with a Rust core library and CLI that can create vaults, add/get/list/edit/rm credentials, sync via git, and manage device keys — all backed by the reference-image + passphrase two-factor KDF.
-**Architecture:** Cargo workspace with two crates: `idfoto-core` (platform-agnostic library — KDF, AEAD, vault format, imgsecret DCT embedding) and `idfoto-cli` (filesystem, git, terminal I/O). The core takes bytes and returns bytes; the CLI handles all platform interaction. TDD throughout.
+**Architecture:** Cargo workspace with two crates: `relicario-core` (platform-agnostic library — KDF, AEAD, vault format, imgsecret DCT embedding) and `relicario-cli` (filesystem, git, terminal I/O). The core takes bytes and returns bytes; the CLI handles all platform interaction. TDD throughout.
**Tech Stack:** Rust (stable, 2021 edition), argon2, chacha20poly1305, image, serde/serde_json, clap, ed25519-dalek
-**Scope:** This is Plan 1 of 2. This plan covers `idfoto-core` and `idfoto-cli`. Plan 2 (idfoto-wasm + Chrome extension) follows after this is working. This plan produces a complete, usable CLI password manager.
+**Scope:** This is Plan 1 of 2. This plan covers `relicario-core` and `relicario-cli`. Plan 2 (relicario-wasm + Chrome extension) follows after this is working. This plan produces a complete, usable CLI password manager.
**Prerequisites:** Rust stable installed via `rustup`. Git installed. A test JPEG image (any cell phone photo) available for manual testing.
-**Design spec:** `docs/superpowers/specs/2026-04-11-idfoto-design.md`
+**Design spec:** `docs/superpowers/specs/2026-04-11-relicario-design.md`
---
## File Structure
```
-idfoto/ (project root = /home/alee/Sources/axsbadge.me)
+relicario/ (project root = /home/alee/Sources/relicario)
├── Cargo.toml # workspace root
├── crates/
-│ ├── idfoto-core/
+│ ├── relicario-core/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # re-exports public API
-│ │ ├── error.rs # IdfotoError enum (thiserror)
+│ │ ├── error.rs # RelicarioError enum (thiserror)
│ │ ├── crypto.rs # derive_master_key(), encrypt(), decrypt()
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs
│ │ ├── vault.rs # encrypt/decrypt entries + manifest, binary format
│ │ └── imgsecret.rs # embed(), extract() — DCT embedding primitive
-│ └── idfoto-cli/
+│ └── relicario-cli/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs # clap CLI with all subcommands
├── docs/
│ └── superpowers/
│ ├── specs/
-│ │ └── 2026-04-11-idfoto-design.md
+│ │ └── 2026-04-11-relicario-design.md
│ └── plans/
-│ └── 2026-04-11-idfoto-core-cli.md (this file)
+│ └── 2026-04-11-relicario-core-cli.md (this file)
└── README.md
```
@@ -50,10 +50,10 @@ idfoto/ (project root = /home/alee/Sources/axsbadge
**Files:**
- Create: `Cargo.toml`
-- Create: `crates/idfoto-core/Cargo.toml`
-- Create: `crates/idfoto-core/src/lib.rs`
-- Create: `crates/idfoto-cli/Cargo.toml`
-- Create: `crates/idfoto-cli/src/main.rs`
+- Create: `crates/relicario-core/Cargo.toml`
+- Create: `crates/relicario-core/src/lib.rs`
+- Create: `crates/relicario-cli/Cargo.toml`
+- Create: `crates/relicario-cli/src/main.rs`
- [ ] **Step 1: Create workspace root Cargo.toml**
@@ -62,20 +62,20 @@ idfoto/ (project root = /home/alee/Sources/axsbadge
[workspace]
resolver = "2"
members = [
- "crates/idfoto-core",
- "crates/idfoto-cli",
+ "crates/relicario-core",
+ "crates/relicario-cli",
]
```
-- [ ] **Step 2: Create idfoto-core crate**
+- [ ] **Step 2: Create relicario-core crate**
```toml
-# crates/idfoto-core/Cargo.toml
+# crates/relicario-core/Cargo.toml
[package]
-name = "idfoto-core"
+name = "relicario-core"
version = "0.1.0"
edition = "2021"
-description = "Core library for idfoto password manager"
+description = "Core library for relicario password manager"
[dependencies]
thiserror = "2"
@@ -92,26 +92,26 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
```
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod error;
```
-- [ ] **Step 3: Create idfoto-cli crate**
+- [ ] **Step 3: Create relicario-cli crate**
```toml
-# crates/idfoto-cli/Cargo.toml
+# crates/relicario-cli/Cargo.toml
[package]
-name = "idfoto-cli"
+name = "relicario-cli"
version = "0.1.0"
edition = "2021"
-description = "CLI for idfoto password manager"
+description = "CLI for relicario password manager"
[[bin]]
-name = "idfoto"
+name = "relicario"
path = "src/main.rs"
[dependencies]
-idfoto-core = { path = "../idfoto-core" }
+relicario-core = { path = "../relicario-core" }
clap = { version = "4", features = ["derive"] }
anyhow = "1"
rpassword = "5"
@@ -120,9 +120,9 @@ dirs = "5"
```
```rust
-// crates/idfoto-cli/src/main.rs
+// crates/relicario-cli/src/main.rs
fn main() {
- println!("idfoto v0.1.0");
+ println!("relicario v0.1.0");
}
```
@@ -138,7 +138,7 @@ git init
echo "target/" > .gitignore
echo ".superpowers/" >> .gitignore
git add Cargo.toml crates/ .gitignore docs/
-git commit -m "feat: scaffold Cargo workspace with idfoto-core and idfoto-cli"
+git commit -m "feat: scaffold Cargo workspace with relicario-core and relicario-cli"
```
---
@@ -146,17 +146,17 @@ git commit -m "feat: scaffold Cargo workspace with idfoto-core and idfoto-cli"
### Task 2: Error Types
**Files:**
-- Create: `crates/idfoto-core/src/error.rs`
-- Modify: `crates/idfoto-core/src/lib.rs`
+- Create: `crates/relicario-core/src/error.rs`
+- Modify: `crates/relicario-core/src/lib.rs`
- [ ] **Step 1: Write the error enum**
```rust
-// crates/idfoto-core/src/error.rs
+// crates/relicario-core/src/error.rs
use thiserror::Error;
#[derive(Debug, Error)]
-pub enum IdfotoError {
+pub enum RelicarioError {
#[error("key derivation failed: {0}")]
Kdf(String),
@@ -193,16 +193,16 @@ pub enum IdfotoError {
DeviceKey(String),
}
-pub type Result = std::result::Result;
+pub type Result = std::result::Result;
```
- [ ] **Step 2: Update lib.rs to re-export**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod error;
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
```
- [ ] **Step 3: Verify build**
@@ -213,8 +213,8 @@ Expected: Compiles cleanly.
- [ ] **Step 4: Commit**
```bash
-git add crates/idfoto-core/src/error.rs crates/idfoto-core/src/lib.rs
-git commit -m "feat: add IdfotoError enum with thiserror"
+git add crates/relicario-core/src/error.rs crates/relicario-core/src/lib.rs
+git commit -m "feat: add RelicarioError enum with thiserror"
```
---
@@ -222,13 +222,13 @@ git commit -m "feat: add IdfotoError enum with thiserror"
### Task 3: Crypto — Key Derivation
**Files:**
-- Create: `crates/idfoto-core/src/crypto.rs`
-- Modify: `crates/idfoto-core/src/lib.rs`
+- Create: `crates/relicario-core/src/crypto.rs`
+- Modify: `crates/relicario-core/src/lib.rs`
- [ ] **Step 1: Write the failing test**
```rust
-// crates/idfoto-core/src/crypto.rs
+// crates/relicario-core/src/crypto.rs
// ... (implementation comes in step 3)
@@ -274,17 +274,17 @@ mod tests {
- [ ] **Step 2: Run test to verify it fails**
-Run: `cargo test -p idfoto-core derive_master_key`
+Run: `cargo test -p relicario-core derive_master_key`
Expected: FAIL — `derive_master_key` and `KdfParams` not defined.
- [ ] **Step 3: Write the implementation**
```rust
-// crates/idfoto-core/src/crypto.rs
+// crates/relicario-core/src/crypto.rs
use argon2::{Algorithm, Argon2, Params, Version};
-use crate::error::{IdfotoError, Result};
+use crate::error::{RelicarioError, Result};
-/// Argon2id tuning parameters. Stored in .idfoto/params.json.
+/// Argon2id tuning parameters. Stored in .relicario/params.json.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KdfParams {
/// Memory cost in KiB (default: 65536 = 64 MiB)
@@ -308,7 +308,7 @@ impl Default for KdfParams {
/// Derive a 32-byte master key from passphrase + image_secret + salt.
///
/// password = passphrase_bytes || image_secret_bytes (concatenated)
-/// salt = vault_salt (32 bytes from .idfoto/salt)
+/// salt = vault_salt (32 bytes from .relicario/salt)
pub fn derive_master_key(
passphrase: &[u8],
image_secret: &[u8; 32],
@@ -326,14 +326,14 @@ pub fn derive_master_key(
params.argon2_p,
Some(32),
)
- .map_err(|e| IdfotoError::Kdf(e.to_string()))?;
+ .map_err(|e| RelicarioError::Kdf(e.to_string()))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
let mut output = [0u8; 32];
argon2
.hash_password_into(&password, salt, &mut output)
- .map_err(|e| IdfotoError::Kdf(e.to_string()))?;
+ .map_err(|e| RelicarioError::Kdf(e.to_string()))?;
Ok(output)
}
@@ -389,24 +389,24 @@ mod tests {
- [ ] **Step 4: Run tests**
-Run: `cargo test -p idfoto-core derive_master_key`
+Run: `cargo test -p relicario-core derive_master_key`
Expected: All 3 tests PASS.
- [ ] **Step 5: Update lib.rs**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod crypto;
pub mod error;
pub use crypto::{derive_master_key, KdfParams};
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
```
- [ ] **Step 6: Commit**
```bash
-git add crates/idfoto-core/src/
+git add crates/relicario-core/src/
git commit -m "feat: add Argon2id key derivation with tests"
```
@@ -415,11 +415,11 @@ git commit -m "feat: add Argon2id key derivation with tests"
### Task 4: Crypto — Encrypt / Decrypt
**Files:**
-- Modify: `crates/idfoto-core/src/crypto.rs`
+- Modify: `crates/relicario-core/src/crypto.rs`
- [ ] **Step 1: Write the failing tests**
-Add to `crates/idfoto-core/src/crypto.rs` inside the `mod tests` block:
+Add to `crates/relicario-core/src/crypto.rs` inside the `mod tests` block:
```rust
#[test]
@@ -471,12 +471,12 @@ Add to `crates/idfoto-core/src/crypto.rs` inside the `mod tests` block:
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core encrypt`
+Run: `cargo test -p relicario-core encrypt`
Expected: FAIL — `encrypt` and `decrypt` not defined.
- [ ] **Step 3: Write the implementation**
-Add to `crates/idfoto-core/src/crypto.rs`, above the `#[cfg(test)]` block:
+Add to `crates/relicario-core/src/crypto.rs`, above the `#[cfg(test)]` block:
```rust
use chacha20poly1305::{
@@ -503,7 +503,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result> {
let ciphertext = cipher
.encrypt(nonce, plaintext)
- .map_err(|_| IdfotoError::Encrypt("XChaCha20-Poly1305 encryption failed".into()))?;
+ .map_err(|_| RelicarioError::Encrypt("XChaCha20-Poly1305 encryption failed".into()))?;
let mut output = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len());
output.push(FORMAT_VERSION);
@@ -518,7 +518,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result> {
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result> {
let min_len = 1 + NONCE_SIZE + 16; // version + nonce + tag (empty plaintext)
if data.len() < min_len {
- return Err(IdfotoError::Format(format!(
+ return Err(RelicarioError::Format(format!(
"ciphertext too short: {} bytes, need at least {}",
data.len(),
min_len
@@ -527,7 +527,7 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result> {
let version = data[0];
if version != FORMAT_VERSION {
- return Err(IdfotoError::Format(format!(
+ return Err(RelicarioError::Format(format!(
"unsupported format version: {version}"
)));
}
@@ -538,7 +538,7 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result> {
let cipher = XChaCha20Poly1305::new(key.into());
cipher
.decrypt(nonce, ciphertext)
- .map_err(|_| IdfotoError::Decrypt)
+ .map_err(|_| RelicarioError::Decrypt)
}
```
@@ -557,24 +557,24 @@ use rand::RngCore;
- [ ] **Step 5: Run all crypto tests**
-Run: `cargo test -p idfoto-core`
+Run: `cargo test -p relicario-core`
Expected: All tests PASS (3 KDF tests + 4 encrypt/decrypt tests).
- [ ] **Step 6: Update lib.rs exports**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod crypto;
pub mod error;
pub use crypto::{derive_master_key, encrypt, decrypt, KdfParams};
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
```
- [ ] **Step 7: Commit**
```bash
-git add crates/idfoto-core/src/
+git add crates/relicario-core/src/
git commit -m "feat: add XChaCha20-Poly1305 encrypt/decrypt with binary format"
```
@@ -583,13 +583,13 @@ git commit -m "feat: add XChaCha20-Poly1305 encrypt/decrypt with binary format"
### Task 5: Entry & Manifest Data Model
**Files:**
-- Create: `crates/idfoto-core/src/entry.rs`
-- Modify: `crates/idfoto-core/src/lib.rs`
+- Create: `crates/relicario-core/src/entry.rs`
+- Modify: `crates/relicario-core/src/lib.rs`
- [ ] **Step 1: Write tests for serialization**
```rust
-// crates/idfoto-core/src/entry.rs
+// crates/relicario-core/src/entry.rs
// ... (implementation in step 3)
@@ -663,13 +663,13 @@ mod tests {
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core entry`
+Run: `cargo test -p relicario-core entry`
Expected: FAIL — types not defined.
- [ ] **Step 3: Write the implementation**
```rust
-// crates/idfoto-core/src/entry.rs
+// crates/relicario-core/src/entry.rs
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -850,25 +850,25 @@ mod tests {
- [ ] **Step 4: Update lib.rs**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod crypto;
pub mod entry;
pub mod error;
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
```
- [ ] **Step 5: Run tests**
-Run: `cargo test -p idfoto-core entry`
+Run: `cargo test -p relicario-core entry`
Expected: All 5 entry tests PASS.
- [ ] **Step 6: Commit**
```bash
-git add crates/idfoto-core/src/
+git add crates/relicario-core/src/
git commit -m "feat: add Entry, Manifest, ManifestEntry data model with serde"
```
@@ -877,13 +877,13 @@ git commit -m "feat: add Entry, Manifest, ManifestEntry data model with serde"
### Task 6: Vault Operations
**Files:**
-- Create: `crates/idfoto-core/src/vault.rs`
-- Modify: `crates/idfoto-core/src/lib.rs`
+- Create: `crates/relicario-core/src/vault.rs`
+- Modify: `crates/relicario-core/src/lib.rs`
- [ ] **Step 1: Write failing tests**
```rust
-// crates/idfoto-core/src/vault.rs
+// crates/relicario-core/src/vault.rs
// ... (implementation in step 3)
@@ -955,13 +955,13 @@ mod tests {
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core vault`
+Run: `cargo test -p relicario-core vault`
Expected: FAIL — functions not defined.
- [ ] **Step 3: Write the implementation**
```rust
-// crates/idfoto-core/src/vault.rs
+// crates/relicario-core/src/vault.rs
use crate::crypto;
use crate::entry::{Entry, Manifest};
use crate::error::Result;
@@ -1061,7 +1061,7 @@ mod tests {
- [ ] **Step 4: Update lib.rs**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod crypto;
pub mod entry;
pub mod error;
@@ -1069,19 +1069,19 @@ pub mod vault;
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
```
- [ ] **Step 5: Run all tests**
-Run: `cargo test -p idfoto-core`
+Run: `cargo test -p relicario-core`
Expected: All tests PASS (KDF + encrypt/decrypt + entry + vault).
- [ ] **Step 6: Commit**
```bash
-git add crates/idfoto-core/src/
+git add crates/relicario-core/src/
git commit -m "feat: add vault encrypt/decrypt for entries and manifest"
```
@@ -1090,15 +1090,15 @@ git commit -m "feat: add vault encrypt/decrypt for entries and manifest"
### Task 7: imgsecret — JPEG Decode, Y Channel, Block DCT
**Files:**
-- Create: `crates/idfoto-core/src/imgsecret.rs`
-- Modify: `crates/idfoto-core/src/lib.rs`
+- Create: `crates/relicario-core/src/imgsecret.rs`
+- Modify: `crates/relicario-core/src/lib.rs`
This task builds the image-processing foundation. No embedding yet — just: load JPEG → extract luminance → divide into 8×8 blocks → DCT forward/inverse.
- [ ] **Step 1: Write tests for DCT round-trip and Y channel extraction**
```rust
-// crates/idfoto-core/src/imgsecret.rs
+// crates/relicario-core/src/imgsecret.rs
// ... (implementation in step 3)
@@ -1179,14 +1179,14 @@ mod tests {
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core imgsecret`
+Run: `cargo test -p relicario-core imgsecret`
Expected: FAIL — functions not defined.
- [ ] **Step 3: Write the implementation**
```rust
-// crates/idfoto-core/src/imgsecret.rs
-use crate::error::{IdfotoError, Result};
+// crates/relicario-core/src/imgsecret.rs
+use crate::error::{RelicarioError, Result};
use image::io::Reader as ImageReader;
use std::f64::consts::PI;
use std::io::Cursor;
@@ -1214,11 +1214,11 @@ pub struct EmbedRegion {
pub fn extract_y_channel(jpeg_bytes: &[u8]) -> Result {
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
.with_guessed_format()
- .map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
+ .map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
let img = reader
.decode()
- .map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
+ .map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
let rgb = img.to_rgb8();
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
@@ -1464,7 +1464,7 @@ mod tests {
- [ ] **Step 4: Update lib.rs**
```rust
-// crates/idfoto-core/src/lib.rs
+// crates/relicario-core/src/lib.rs
pub mod crypto;
pub mod entry;
pub mod error;
@@ -1473,19 +1473,19 @@ pub mod vault;
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
-pub use error::{IdfotoError, Result};
+pub use error::{RelicarioError, Result};
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
```
- [ ] **Step 5: Run tests**
-Run: `cargo test -p idfoto-core imgsecret`
+Run: `cargo test -p relicario-core imgsecret`
Expected: All 4 tests PASS.
- [ ] **Step 6: Commit**
```bash
-git add crates/idfoto-core/src/
+git add crates/relicario-core/src/
git commit -m "feat: add imgsecret JPEG decode, Y channel extraction, and 8x8 DCT"
```
@@ -1494,7 +1494,7 @@ git commit -m "feat: add imgsecret JPEG decode, Y channel extraction, and 8x8 DC
### Task 8: imgsecret — QIM Embedding + Block Selection
**Files:**
-- Modify: `crates/idfoto-core/src/imgsecret.rs`
+- Modify: `crates/relicario-core/src/imgsecret.rs`
This task adds QIM (Quantization Index Modulation) for embedding/extracting individual bits in DCT coefficients, and the fixed geometric pattern for selecting which blocks carry data.
@@ -1544,7 +1544,7 @@ Add to `mod tests` in `imgsecret.rs`:
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core qim`
+Run: `cargo test -p relicario-core qim`
Expected: FAIL — `qim_embed`, `qim_extract`, `select_embed_blocks`, `QUANT_STEP` not defined.
- [ ] **Step 3: Write QIM and block selection implementation**
@@ -1632,13 +1632,13 @@ pub fn select_embed_blocks(region: &EmbedRegion, target_count: usize) -> Vec<(us
- [ ] **Step 4: Run tests**
-Run: `cargo test -p idfoto-core imgsecret`
+Run: `cargo test -p relicario-core imgsecret`
Expected: All tests PASS (previous 4 + 3 new QIM/block-selection tests).
- [ ] **Step 5: Commit**
```bash
-git add crates/idfoto-core/src/imgsecret.rs
+git add crates/relicario-core/src/imgsecret.rs
git commit -m "feat: add QIM bit embedding and fixed-pattern block selection"
```
@@ -1647,7 +1647,7 @@ git commit -m "feat: add QIM bit embedding and fixed-pattern block selection"
### Task 9: imgsecret — Full embed() and extract()
**Files:**
-- Modify: `crates/idfoto-core/src/imgsecret.rs`
+- Modify: `crates/relicario-core/src/imgsecret.rs`
This is the main event: the public `embed()` and `extract()` functions with redundancy coding and majority voting. Reed-Solomon is added in Task 10.
@@ -1697,7 +1697,7 @@ Add `use rand::Fill;` at the top of the test module for the random fill.
- [ ] **Step 2: Run tests to verify they fail**
-Run: `cargo test -p idfoto-core embed_extract`
+Run: `cargo test -p relicario-core embed_extract`
Expected: FAIL — `embed` and `extract` not defined.
- [ ] **Step 3: Write embed() implementation**
@@ -1727,7 +1727,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result> {
// Check minimum size
if y.width < MIN_DIMENSION as usize || y.height < MIN_DIMENSION as usize {
- return Err(IdfotoError::ImageTooSmall {
+ return Err(RelicarioError::ImageTooSmall {
min_width: MIN_DIMENSION,
min_height: MIN_DIMENSION,
actual_width: y.width as u32,
@@ -1739,7 +1739,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result> {
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50); // cap at 50 copies
if num_copies < MIN_COPIES {
- return Err(IdfotoError::ImgSecret(format!(
+ return Err(RelicarioError::ImgSecret(format!(
"image too small for embedding: only {num_copies} copies fit, need at least {MIN_COPIES}"
)));
}
@@ -1793,7 +1793,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
let new_x = region.x_offset as isize + dx;
let new_y = region.y_offset as isize + dy;
if new_x < 0 || new_y < 0 {
- return Err(IdfotoError::ExtractionFailed);
+ return Err(RelicarioError::ExtractionFailed);
}
region.x_offset = new_x as usize;
region.y_offset = new_y as usize;
@@ -1808,7 +1808,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50);
if num_copies < 1 {
- return Err(IdfotoError::ExtractionFailed);
+ return Err(RelicarioError::ExtractionFailed);
}
let blocks_needed = num_copies * BLOCKS_PER_COPY;
@@ -1857,7 +1857,7 @@ fn extract_at_offset(jpeg_bytes: &[u8], dx: isize, dy: isize) -> Result<[u8; 32]
let total_votes: u32 = bit_votes.iter().map(|v| v[0] + v[1]).sum();
let min_confidence = total_votes * 3 / 4; // at least 75% of votes should agree
if confidence < min_confidence {
- return Err(IdfotoError::ExtractionFailed);
+ return Err(RelicarioError::ExtractionFailed);
}
Ok(bits_to_bytes(&secret_bits))
@@ -1894,11 +1894,11 @@ fn bits_to_bytes(bits: &[u8]) -> [u8; 32] {
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result> {
let reader = ImageReader::new(Cursor::new(original_jpeg))
.with_guessed_format()
- .map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
+ .map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
let img = reader
.decode()
- .map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
+ .map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
let rgb = img.to_rgb8();
let (width, height) = (rgb.width(), rgb.height());
@@ -1933,14 +1933,14 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result Result<[u8; 32]> {
}
}
- Err(IdfotoError::ExtractionFailed)
+ Err(RelicarioError::ExtractionFailed)
}
```
@@ -2110,19 +2110,19 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
}
}
- Err(IdfotoError::ExtractionFailed)
+ Err(RelicarioError::ExtractionFailed)
}
```
- [ ] **Step 4: Run all imgsecret tests**
-Run: `cargo test -p idfoto-core imgsecret -- --nocapture`
+Run: `cargo test -p relicario-core imgsecret -- --nocapture`
Expected: All tests PASS including crop recovery.
- [ ] **Step 5: Commit**
```bash
-git add crates/idfoto-core/src/imgsecret.rs
+git add crates/relicario-core/src/imgsecret.rs
git commit -m "feat: add crop recovery with multi-offset extraction search"
```
@@ -2131,15 +2131,15 @@ git commit -m "feat: add crop recovery with multi-offset extraction search"
### Task 11: CLI — Scaffolding, init, generate
**Files:**
-- Modify: `crates/idfoto-cli/src/main.rs`
+- Modify: `crates/relicario-cli/src/main.rs`
- [ ] **Step 1: Write the clap CLI structure**
```rust
-// crates/idfoto-cli/src/main.rs
+// crates/relicario-cli/src/main.rs
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
-use idfoto_core::{
+use relicario_core::{
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
};
@@ -2148,7 +2148,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Parser)]
-#[command(name = "idfoto", version, about = "Git-backed password manager with reference image authentication")]
+#[command(name = "relicario", version, about = "Git-backed password manager with reference image authentication")]
struct Cli {
#[command(subcommand)]
command: Commands,
@@ -2230,21 +2230,21 @@ fn vault_dir() -> PathBuf {
PathBuf::from(".")
}
-fn idfoto_dir() -> PathBuf {
- vault_dir().join(".idfoto")
+fn relicario_dir() -> PathBuf {
+ vault_dir().join(".relicario")
}
fn read_salt() -> Result<[u8; 32]> {
- let bytes = fs::read(idfoto_dir().join("salt"))
- .context("failed to read .idfoto/salt — is this a vault directory?")?;
+ let bytes = fs::read(relicario_dir().join("salt"))
+ .context("failed to read .relicario/salt — is this a vault directory?")?;
let mut salt = [0u8; 32];
salt.copy_from_slice(&bytes);
Ok(salt)
}
fn read_params() -> Result {
- let json = fs::read_to_string(idfoto_dir().join("params.json"))
- .context("failed to read .idfoto/params.json")?;
+ let json = fs::read_to_string(relicario_dir().join("params.json"))
+ .context("failed to read .relicario/params.json")?;
Ok(serde_json::from_str(&json)?)
}
@@ -2256,7 +2256,7 @@ fn unlock(image_path: &Path) -> Result<[u8; 32]> {
let jpeg_bytes = fs::read(image_path)
.context("failed to read reference image")?;
- let image_secret = idfoto_core::imgsecret::extract(&jpeg_bytes)
+ let image_secret = relicario_core::imgsecret::extract(&jpeg_bytes)
.map_err(|e| anyhow::anyhow!("failed to extract image secret: {e}"))?;
let salt = read_salt()?;
@@ -2268,9 +2268,9 @@ fn unlock(image_path: &Path) -> Result<[u8; 32]> {
Ok(master_key)
}
-/// Get reference image path — from env var IDFOTO_IMAGE or prompt.
+/// Get reference image path — from env var RELICARIO_IMAGE or prompt.
fn get_image_path() -> Result {
- if let Ok(path) = std::env::var("IDFOTO_IMAGE") {
+ if let Ok(path) = std::env::var("RELICARIO_IMAGE") {
return Ok(PathBuf::from(path));
}
eprint!("Reference image path: ");
@@ -2328,7 +2328,7 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut image_secret);
println!("Embedding secret into reference image...");
- let stego_jpeg = idfoto_core::imgsecret::embed(&carrier_jpeg, &image_secret)
+ let stego_jpeg = relicario_core::imgsecret::embed(&carrier_jpeg, &image_secret)
.map_err(|e| anyhow::anyhow!("failed to embed secret: {e}"))?;
fs::write(output_path, &stego_jpeg)
.context("failed to write reference image")?;
@@ -2354,14 +2354,14 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
.map_err(|e| anyhow::anyhow!("key derivation failed: {e}"))?;
// 5. Write vault structure
- fs::create_dir_all(idfoto_dir())?;
+ fs::create_dir_all(relicario_dir())?;
fs::create_dir_all(vault_dir().join("entries"))?;
- fs::write(idfoto_dir().join("salt"), salt)?;
+ fs::write(relicario_dir().join("salt"), salt)?;
fs::write(
- idfoto_dir().join("params.json"),
+ relicario_dir().join("params.json"),
serde_json::to_string_pretty(¶ms)?,
)?;
- fs::write(idfoto_dir().join("devices.json"), "[]")?;
+ fs::write(relicario_dir().join("devices.json"), "[]")?;
// 6. Write empty manifest
let manifest = Manifest::new();
@@ -2373,7 +2373,7 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
// Add .gitignore
fs::write(vault_dir().join(".gitignore"), "reference.jpg\n")?;
- git_commit("feat: initialize idfoto vault")?;
+ git_commit("feat: initialize relicario vault")?;
println!("\nVault initialized successfully!");
println!("IMPORTANT: Keep your reference image ({}) safe — you need it to unlock the vault.", output_path.display());
@@ -2664,7 +2664,7 @@ Expected: Shows all subcommands with descriptions.
- [ ] **Step 5: Commit**
```bash
-git add crates/idfoto-cli/src/main.rs
+git add crates/relicario-cli/src/main.rs
git commit -m "feat: add full CLI with init, add, get, list, edit, rm, sync, generate"
```
@@ -2673,7 +2673,7 @@ git commit -m "feat: add full CLI with init, add, get, list, edit, rm, sync, gen
### Task 12: CLI — Device Management
**Files:**
-- Modify: `crates/idfoto-cli/src/main.rs`
+- Modify: `crates/relicario-cli/src/main.rs`
- [ ] **Step 1: Add device subcommands to the CLI**
@@ -2733,14 +2733,14 @@ struct DeviceEntry {
}
fn read_devices() -> Result> {
- let json = fs::read_to_string(idfoto_dir().join("devices.json"))
+ let json = fs::read_to_string(relicario_dir().join("devices.json"))
.context("failed to read devices.json")?;
Ok(serde_json::from_str(&json)?)
}
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
let json = serde_json::to_string_pretty(devices)?;
- fs::write(idfoto_dir().join("devices.json"), json)?;
+ fs::write(relicario_dir().join("devices.json"), json)?;
Ok(())
}
@@ -2759,7 +2759,7 @@ fn cmd_device_add(name: &str) -> Result<()> {
// Save private key to local config
let config_dir = dirs::config_dir()
.context("no config directory")?
- .join("idfoto");
+ .join("relicario");
fs::create_dir_all(&config_dir)?;
fs::write(
config_dir.join(format!("{name}.key")),
@@ -2806,7 +2806,7 @@ fn cmd_device_revoke(name: &str) -> Result<()> {
- [ ] **Step 3: Add hex dependency**
-Add to `crates/idfoto-cli/Cargo.toml` under `[dependencies]`:
+Add to `crates/relicario-cli/Cargo.toml` under `[dependencies]`:
```toml
hex = "0.4"
@@ -2824,7 +2824,7 @@ Expected: Compiles cleanly.
- [ ] **Step 5: Commit**
```bash
-git add crates/idfoto-cli/
+git add crates/relicario-cli/
git commit -m "feat: add device add/list/revoke commands with ed25519 key management"
```
@@ -2833,16 +2833,16 @@ git commit -m "feat: add device add/list/revoke commands with ed25519 key manage
### Task 13: Integration Test — Full Vault Workflow
**Files:**
-- Create: `crates/idfoto-core/tests/integration.rs`
+- Create: `crates/relicario-core/tests/integration.rs`
This test exercises the full flow: generate secret → embed → derive key → encrypt entry → decrypt entry → extract secret from re-encoded image.
- [ ] **Step 1: Write the integration test**
```rust
-// crates/idfoto-core/tests/integration.rs
-use idfoto_core::*;
-use idfoto_core::imgsecret;
+// crates/relicario-core/tests/integration.rs
+use relicario_core::*;
+use relicario_core::imgsecret;
fn make_test_jpeg(width: u32, height: u32) -> Vec {
use image::codecs::jpeg::JpegEncoder;
@@ -2967,7 +2967,7 @@ fn two_factor_independence() {
- [ ] **Step 2: Run integration tests**
-Run: `cargo test -p idfoto-core --test integration`
+Run: `cargo test -p relicario-core --test integration`
Expected: Both tests PASS.
- [ ] **Step 3: Run the full test suite**
@@ -2978,7 +2978,7 @@ Expected: ALL tests across all crates PASS.
- [ ] **Step 4: Commit**
```bash
-git add crates/idfoto-core/tests/
+git add crates/relicario-core/tests/
git commit -m "test: add full-workflow integration test and two-factor independence verification"
```
@@ -2987,7 +2987,7 @@ git commit -m "test: add full-workflow integration test and two-factor independe
## Plan 2 Preview
After this plan is complete and passing, Plan 2 covers:
-- **idfoto-wasm**: wasm-bindgen wrapper around idfoto-core (compile with `wasm-pack build`)
+- **relicario-wasm**: wasm-bindgen wrapper around relicario-core (compile with `wasm-pack build`)
- **Chrome MV3 extension**: TypeScript popup + content script + service worker, loading the WASM module for inline crypto
- **Extension UX**: passphrase prompt, entry list/search, autofill detection
diff --git a/docs/superpowers/plans/2026-04-12-idfoto-credential-capture.md b/docs/superpowers/plans/2026-04-12-relicario-credential-capture.md
similarity index 96%
rename from docs/superpowers/plans/2026-04-12-idfoto-credential-capture.md
rename to docs/superpowers/plans/2026-04-12-relicario-credential-capture.md
index b22d45c..ceeff94 100644
--- a/docs/superpowers/plans/2026-04-12-idfoto-credential-capture.md
+++ b/docs/superpowers/plans/2026-04-12-relicario-credential-capture.md
@@ -1,4 +1,4 @@
-# idfoto Credential Capture Implementation Plan
+# relicario Credential Capture Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
@@ -8,7 +8,7 @@
**Tech Stack:** TypeScript, Chrome extension APIs, DOM injection
-**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-credential-capture-design.md`
+**Spec:** `docs/superpowers/specs/2026-04-12-relicario-credential-capture-design.md`
---
@@ -24,7 +24,7 @@ extension/src/popup/components/settings.ts # Settings view
### Modified files
```
-extension/src/shared/types.ts # Add IdfotoSettings interface
+extension/src/shared/types.ts # Add RelicarioSettings interface
extension/src/shared/messages.ts # Add new message types
extension/src/service-worker/index.ts # Handle new messages
extension/src/content/detector.ts # Import and init capture
@@ -40,17 +40,17 @@ extension/src/popup/components/unlock.ts # Wire settings button to settings vie
- Modify: `extension/src/shared/types.ts`
- Modify: `extension/src/shared/messages.ts`
-- [ ] **Step 1: Add IdfotoSettings to types.ts**
+- [ ] **Step 1: Add RelicarioSettings to types.ts**
Add at the end of `extension/src/shared/types.ts`:
```typescript
-export interface IdfotoSettings {
+export interface RelicarioSettings {
captureEnabled: boolean;
captureStyle: 'bar' | 'toast';
}
-export const DEFAULT_SETTINGS: IdfotoSettings = {
+export const DEFAULT_SETTINGS: RelicarioSettings = {
captureEnabled: false,
captureStyle: 'bar',
};
@@ -64,7 +64,7 @@ Add these to the `Request` union in `extension/src/shared/messages.ts`:
| { type: 'check_credential'; url: string; username: string; password: string }
| { type: 'blacklist_site'; hostname: string }
| { type: 'get_settings' }
-| { type: 'update_settings'; settings: Partial }
+| { type: 'update_settings'; settings: Partial }
| { type: 'get_blacklist' }
| { type: 'remove_blacklist'; hostname: string }
```
@@ -88,16 +88,16 @@ git commit -m "feat: add settings and credential capture message types"
Add these helper functions to `extension/src/service-worker/index.ts`, after the existing storage helpers:
```typescript
-import type { IdfotoSettings } from '../shared/types';
+import type { RelicarioSettings } from '../shared/types';
import { DEFAULT_SETTINGS } from '../shared/types';
-async function loadSettings(): Promise {
+async function loadSettings(): Promise {
const data = await chrome.storage.local.get(['settings']);
if (!data.settings) return { ...DEFAULT_SETTINGS };
return { ...DEFAULT_SETTINGS, ...data.settings };
}
-async function saveSettings(settings: IdfotoSettings): Promise {
+async function saveSettings(settings: RelicarioSettings): Promise {
await chrome.storage.local.set({ settings });
}
@@ -356,9 +356,9 @@ export function hookForms(): void {
// --- Prompt UI ---
-/// Remove any existing idfoto prompt from the page.
+/// Remove any existing relicario prompt from the page.
function removePrompt(): void {
- document.getElementById('idfoto-capture-prompt')?.remove();
+ document.getElementById('relicario-capture-prompt')?.remove();
}
/// Show a save/update prompt.
@@ -385,7 +385,7 @@ function showPrompt(
: `Save login for ${hostname}?`;
const container = document.createElement('div');
- container.id = 'idfoto-capture-prompt';
+ container.id = 'relicario-capture-prompt';
// Common styles
const baseStyles = `
@@ -451,7 +451,7 @@ function showPrompt(
// Brand label
const brand = document.createElement('span');
- brand.textContent = 'idfoto';
+ brand.textContent = 'relicario';
brand.style.cssText = 'color: #58a6ff; font-weight: normal; letter-spacing: 1px;';
// Message text
@@ -627,7 +627,7 @@ Create `extension/src/popup/components/settings.ts`:
/// Settings view — configure credential capture and manage blacklist.
import { setState, sendMessage, navigate, escapeHtml } from '../popup';
-import type { IdfotoSettings } from '../../shared/types';
+import type { RelicarioSettings } from '../../shared/types';
export async function renderSettings(app: HTMLElement): Promise {
// Load current settings and blacklist in parallel.
@@ -636,8 +636,8 @@ export async function renderSettings(app: HTMLElement): Promise {
sendMessage({ type: 'get_blacklist' }),
]);
- const settings: IdfotoSettings = settingsResp.ok
- ? settingsResp.data as IdfotoSettings
+ const settings: RelicarioSettings = settingsResp.ok
+ ? settingsResp.data as RelicarioSettings
: { captureEnabled: false, captureStyle: 'bar' };
const blacklist: string[] = blacklistResp.ok
diff --git a/docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md b/docs/superpowers/plans/2026-04-12-relicario-firefox-extension.md
similarity index 90%
rename from docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md
rename to docs/superpowers/plans/2026-04-12-relicario-firefox-extension.md
index c6a93ff..2ea1ca5 100644
--- a/docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md
+++ b/docs/superpowers/plans/2026-04-12-relicario-firefox-extension.md
@@ -1,4 +1,4 @@
-# idfoto Firefox Extension Port Implementation Plan
+# relicario Firefox Extension Port Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
@@ -8,7 +8,7 @@
**Tech Stack:** TypeScript, webpack, Firefox WebExtensions MV3
-**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md`
+**Spec:** `docs/superpowers/specs/2026-04-12-relicario-firefox-extension-design.md`
---
@@ -46,12 +46,12 @@ Create `extension/manifest.firefox.json`:
```json
{
"manifest_version": 3,
- "name": "idfoto",
+ "name": "relicario",
"version": "0.1.0",
"description": "Two-factor encrypted password manager",
"browser_specific_settings": {
"gecko": {
- "id": "idfoto@adlee.work",
+ "id": "relicario@adlee.work",
"strict_min_version": "128.0"
}
},
@@ -84,8 +84,8 @@ Create `extension/manifest.firefox.json`:
"setup.html",
"setup.js",
"styles.css",
- "idfoto_wasm_bg.wasm",
- "idfoto_wasm.js"
+ "relicario_wasm_bg.wasm",
+ "relicario_wasm.js"
]
}
]
@@ -126,8 +126,8 @@ module.exports = {
{ from: 'src/popup/styles.css', to: 'styles.css' },
{ from: 'setup.html', to: '.' },
{ from: 'icons', to: 'icons' },
- { from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
- { from: 'wasm/idfoto_wasm.js', to: '.' },
+ { from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
+ { from: 'wasm/relicario_wasm.js', to: '.' },
],
}),
],
@@ -147,7 +147,7 @@ In `extension/package.json`, update the `scripts` section:
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
"dev": "webpack --mode development --watch",
"dev:firefox": "webpack --config webpack.firefox.config.js --mode development --watch",
- "build:wasm": "wasm-pack build ../crates/idfoto-wasm --target web --out-dir ../../extension/wasm"
+ "build:wasm": "wasm-pack build ../crates/relicario-wasm --target web --out-dir ../../extension/wasm"
}
}
```
@@ -189,9 +189,9 @@ In `extension/src/service-worker/index.ts`, replace the current `initWasm` funct
// (Chrome) and the default export (Firefox) are available.
// @ts-ignore TS2307 — resolved by webpack alias / copy
-import initDefault, { initSync } from '../../wasm/idfoto_wasm.js';
+import initDefault, { initSync } from '../../wasm/relicario_wasm.js';
// @ts-ignore TS2307
-import * as wasmBindings from '../../wasm/idfoto_wasm.js';
+import * as wasmBindings from '../../wasm/relicario_wasm.js';
type WasmModule = typeof wasmBindings;
let wasm: WasmModule | null = null;
@@ -204,12 +204,12 @@ async function initWasm(): Promise {
if (isServiceWorker) {
// Chrome: fetch WASM binary and instantiate synchronously
- const wasmResponse = await fetch(chrome.runtime.getURL('idfoto_wasm_bg.wasm'));
+ const wasmResponse = await fetch(chrome.runtime.getURL('relicario_wasm_bg.wasm'));
const wasmBytes = await wasmResponse.arrayBuffer();
initSync({ module: new WebAssembly.Module(wasmBytes) });
} else {
// Firefox: background script — dynamic init works
- const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm');
+ const wasmUrl = chrome.runtime.getURL('relicario_wasm_bg.wasm');
await initDefault(wasmUrl);
}
@@ -225,13 +225,13 @@ async function initWasm(): Promise {
Change the doc comment at the top of the file (line 1) from:
```typescript
-/// Service worker entry point for the idfoto Chrome extension.
+/// Service worker entry point for the relicario Chrome extension.
```
To:
```typescript
-/// Background script entry point for the idfoto browser extension.
+/// Background script entry point for the relicario browser extension.
///
/// In Chrome this runs as a service worker (MV3). In Firefox this runs
/// as a persistent background script. WASM loading adapts automatically.
diff --git a/docs/superpowers/plans/2026-04-12-idfoto-init-wizard.md b/docs/superpowers/plans/2026-04-12-relicario-init-wizard.md
similarity index 92%
rename from docs/superpowers/plans/2026-04-12-idfoto-init-wizard.md
rename to docs/superpowers/plans/2026-04-12-relicario-init-wizard.md
index a7cbe68..adae01f 100644
--- a/docs/superpowers/plans/2026-04-12-idfoto-init-wizard.md
+++ b/docs/superpowers/plans/2026-04-12-relicario-init-wizard.md
@@ -1,14 +1,14 @@
-# idfoto Vault Initialization Wizard Implementation Plan
+# relicario Vault Initialization Wizard Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-**Goal:** Build a browser-based wizard that creates a new idfoto vault, pushes it to Gitea/GitHub via API, downloads the reference image, and optionally configures the Chrome extension.
+**Goal:** Build a browser-based wizard that creates a new relicario vault, pushes it to Gitea/GitHub via API, downloads the reference image, and optionally configures the Chrome extension.
**Architecture:** Single HTML page (`extension/setup.html`) bundled by webpack as a new entry point. Reuses the existing git API layer and WASM module. New `embed_image_secret` function added to the WASM crate. The wizard runs entirely client-side — all crypto happens in the browser via WASM.
**Tech Stack:** TypeScript, wasm-bindgen (existing WASM crate), webpack, Chrome extension APIs
-**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md`
+**Spec:** `docs/superpowers/specs/2026-04-12-relicario-init-wizard-design.md`
---
@@ -17,7 +17,7 @@
### Rust (modified)
```
-crates/idfoto-wasm/src/lib.rs # Add embed_image_secret function
+crates/relicario-wasm/src/lib.rs # Add embed_image_secret function
```
### Extension (new)
@@ -42,16 +42,16 @@ extension/manifest.json # Add web_accessible_resources for setup.html
## Task 1: Add `embed_image_secret` to WASM Crate
**Files:**
-- Modify: `crates/idfoto-wasm/src/lib.rs`
+- Modify: `crates/relicario-wasm/src/lib.rs`
- [ ] **Step 1: Write the test**
-Add to the `#[cfg(test)] mod tests` block in `crates/idfoto-wasm/src/lib.rs`:
+Add to the `#[cfg(test)] mod tests` block in `crates/relicario-wasm/src/lib.rs`:
```rust
#[test]
fn embed_then_extract_round_trip() {
- // Create a synthetic test JPEG (same approach as idfoto-core tests)
+ // Create a synthetic test JPEG (same approach as relicario-core tests)
use image::codecs::jpeg::JpegEncoder;
use image::{ImageBuffer, ImageEncoder, Rgb};
@@ -81,12 +81,12 @@ fn embed_then_extract_round_trip() {
- [ ] **Step 2: Run test to verify it fails**
-Run: `cargo test -p idfoto-wasm embed_then_extract`
+Run: `cargo test -p relicario-wasm embed_then_extract`
Expected: FAIL — `embed_image_secret` not defined.
- [ ] **Step 3: Add `image` dev-dependency to Cargo.toml**
-Add to `crates/idfoto-wasm/Cargo.toml` under `[dev-dependencies]`:
+Add to `crates/relicario-wasm/Cargo.toml` under `[dev-dependencies]`:
```toml
[dev-dependencies]
@@ -96,7 +96,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
- [ ] **Step 4: Implement the function**
-Add to `crates/idfoto-wasm/src/lib.rs`, after the `extract_image_secret` function:
+Add to `crates/relicario-wasm/src/lib.rs`, after the `extract_image_secret` function:
```rust
/// Embed a 256-bit secret into a carrier JPEG image.
@@ -111,25 +111,25 @@ pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result,
let secret: [u8; 32] = secret
.try_into()
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
- idfoto_core::imgsecret::embed(carrier_jpeg, &secret)
+ relicario_core::imgsecret::embed(carrier_jpeg, &secret)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
```
- [ ] **Step 5: Run test to verify it passes**
-Run: `cargo test -p idfoto-wasm embed_then_extract`
+Run: `cargo test -p relicario-wasm embed_then_extract`
Expected: PASS
- [ ] **Step 6: Rebuild WASM**
-Run: `wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm`
+Run: `wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm`
Expected: Builds successfully.
- [ ] **Step 7: Commit**
```bash
-git add crates/idfoto-wasm/src/lib.rs crates/idfoto-wasm/Cargo.toml
+git add crates/relicario-wasm/src/lib.rs crates/relicario-wasm/Cargo.toml
git commit -m "feat: add embed_image_secret to WASM crate"
```
@@ -172,7 +172,7 @@ Create `extension/setup.html`:
- idfoto — vault setup
+ relicario — vault setup