chore: rename project from idfoto to relicario

Sweeping rename across crates, CLI binary, WASM bindings, extension, docs,
and vault metadata paths. Git remote updated to relicario.git.

- crates/idfoto-{core,cli,wasm} -> crates/relicario-{core,cli,wasm}
- IdfotoError -> RelicarioError
- IDFOTO_IMAGE env var -> RELICARIO_IMAGE
- ~/.config/idfoto -> ~/.config/relicario
- .idfoto/ vault metadata dir -> .relicario/ (breaking; pre-release)
- Binary name idfoto -> relicario
- Extension wasm module idfoto_wasm -> relicario_wasm
- Storage key idfotoSettings -> relicarioSettings
- All doc filenames and content references updated

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-19 16:47:02 -04:00
parent 20ff1d9f47
commit 519a6f0e36
51 changed files with 949 additions and 949 deletions

View File

@@ -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/idfoto)
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/idfoto)
**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/idfoto)
[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<T> = std::result::Result<T, IdfotoError>;
pub type Result<T> = std::result::Result<T, RelicarioError>;
```
- [ ] **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<Vec<u8>> {
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<Vec<u8>> {
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<YChannel> {
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<Vec<u8>> {
// 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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
encoder
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
.map_err(|e| IdfotoError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
.map_err(|e| RelicarioError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
Ok(buf)
}
```
- [ ] **Step 4: Run tests**
Run: `cargo test -p idfoto-core imgsecret -- --nocapture`
Run: `cargo test -p relicario-core imgsecret -- --nocapture`
Expected: All tests PASS including embed/extract round-trip.
- [ ] **Step 5: Add a JPEG recompression survival test**
@@ -1976,13 +1976,13 @@ Add to `mod tests`:
- [ ] **Step 6: Run all tests**
Run: `cargo test -p idfoto-core`
Run: `cargo test -p relicario-core`
Expected: All tests PASS.
- [ ] **Step 7: Commit**
```bash
git add crates/idfoto-core/src/imgsecret.rs
git add crates/relicario-core/src/imgsecret.rs
git commit -m "feat: add imgsecret embed/extract with redundancy and majority voting"
```
@@ -1991,7 +1991,7 @@ git commit -m "feat: add imgsecret embed/extract with redundancy and majority vo
### Task 10: imgsecret — Crop Recovery
**Files:**
- Modify: `crates/idfoto-core/src/imgsecret.rs`
- Modify: `crates/relicario-core/src/imgsecret.rs`
- [ ] **Step 1: Write failing crop test**
@@ -2033,7 +2033,7 @@ Add to `mod tests`:
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p idfoto-core crop`
Run: `cargo test -p relicario-core crop`
Expected: FAIL — `extract_with_crop_recovery` not defined.
- [ ] **Step 3: Write crop recovery implementation**
@@ -2066,7 +2066,7 @@ pub fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> 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<KdfParams> {
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<PathBuf> {
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(&params)?,
)?;
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<Vec<DeviceEntry>> {
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<u8> {
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

View File

@@ -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<import('./types').IdfotoSettings> }
| { type: 'update_settings'; settings: Partial<import('./types').RelicarioSettings> }
| { 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<IdfotoSettings> {
async function loadSettings(): Promise<RelicarioSettings> {
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<void> {
async function saveSettings(settings: RelicarioSettings): Promise<void> {
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<void> {
// Load current settings and blacklist in parallel.
@@ -636,8 +636,8 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
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

View File

@@ -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<WasmModule> {
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<WasmModule> {
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.

View File

@@ -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<Vec<u8>,
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`:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>idfoto — vault setup</title>
<title>relicario — vault setup</title>
<link rel="stylesheet" href="styles.css">
<style>
/* Override popup constraints for full-page layout */
@@ -339,12 +339,12 @@ let state: WizardState = {
// --- WASM ---
type WasmModule = typeof import('idfoto-wasm');
type WasmModule = typeof import('relicario-wasm');
let wasm: WasmModule | null = null;
async function initWasm(): Promise<WasmModule> {
if (wasm) return wasm;
const mod = await import(/* webpackIgnore: true */ '../idfoto_wasm.js');
const mod = await import(/* webpackIgnore: true */ '../relicario_wasm.js');
await mod.default();
wasm = mod;
return mod;
@@ -378,7 +378,7 @@ function render(): void {
const stepNames = ['git host', 'connection', 'create vault', 'done'];
let html = `
<div class="brand" style="font-size:18px;margin-bottom:4px">idfoto setup</div>
<div class="brand" style="font-size:18px;margin-bottom:4px">relicario setup</div>
<div class="wizard-step">step ${state.step} of 4 — ${stepNames[state.step - 1]}</div>
<div class="progress-bar"><div class="progress-bar-fill" style="width:${(state.step / 4) * 100}%"></div></div>
`;
@@ -416,7 +416,7 @@ function renderStep1(): string {
<ol>
<li>Log in to your Gitea instance</li>
<li>Click <code>+</code> → <code>New Repository</code></li>
<li>Name it (e.g. <code>idfoto-vault</code>), leave it <strong>empty</strong> — no README, no .gitignore</li>
<li>Name it (e.g. <code>relicario-vault</code>), leave it <strong>empty</strong> — no README, no .gitignore</li>
<li>Go to <code>Settings</code> → <code>Applications</code> → <code>Manage Access Tokens</code></li>
<li>Generate a new token with <code>repo</code> scope (read/write)</li>
<li>Copy the token — you'll need it in the next step</li>
@@ -425,7 +425,7 @@ function renderStep1(): string {
<div class="label" style="margin-bottom:8px">GITHUB SETUP</div>
<ol>
<li>Go to <strong>github.com</strong> → <code>New Repository</code></li>
<li>Name it (e.g. <code>idfoto-vault</code>), set to <strong>Private</strong>, leave it <strong>empty</strong> — no README, no .gitignore, no license</li>
<li>Name it (e.g. <code>relicario-vault</code>), set to <strong>Private</strong>, leave it <strong>empty</strong> — no README, no .gitignore, no license</li>
<li>Go to <code>Settings</code> → <code>Developer Settings</code> → <code>Personal Access Tokens</code> → <code>Fine-grained tokens</code></li>
<li>Click <code>Generate new token</code></li>
<li>Select <strong>only</strong> the vault repository under "Repository access"</li>
@@ -534,7 +534,7 @@ function renderStep4(): string {
</p>
` : `
<p class="secondary" style="font-size:11px;margin-bottom:8px">
idfoto extension detected. Push your vault config to it?
relicario extension detected. Push your vault config to it?
</p>
<button class="btn" data-action="push-to-extension">Configure Extension</button>
`}
@@ -543,7 +543,7 @@ function renderStep4(): string {
<div class="form-group" style="margin-top:16px">
<div class="label">EXTENSION SETUP</div>
<p class="secondary" style="font-size:11px;margin-bottom:8px">
Install the idfoto extension, then enter these details in the setup wizard:
Install the relicario extension, then enter these details in the setup wizard:
</p>
<div class="config-blob" data-action="copy-config" title="Click to copy">
${escapeHtml(JSON.stringify({
@@ -796,9 +796,9 @@ async function createVault(): Promise<void> {
const manifestEnc = w.encrypt_manifest(emptyManifest, masterKey);
// 7. Push vault files to repo
await git.writeFile('.idfoto/salt', salt, 'feat: initialize idfoto vault');
await git.writeFile('.idfoto/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
await git.writeFile('.idfoto/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
await git.writeFile('.relicario/salt', salt, 'feat: initialize relicario vault');
await git.writeFile('.relicario/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
await git.writeFile('.relicario/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
await git.writeFile('manifest.enc', new Uint8Array(manifestEnc), 'feat: add encrypted manifest');
}
@@ -872,7 +872,7 @@ Add to `extension/manifest.json`, after the `content_security_policy` block:
```json
"web_accessible_resources": [{
"resources": ["setup.html", "setup.js", "styles.css", "idfoto_wasm_bg.wasm", "idfoto_wasm.js"],
"resources": ["setup.html", "setup.js", "styles.css", "relicario_wasm_bg.wasm", "relicario_wasm.js"],
"matches": ["<all_urls>"]
}]
```
@@ -901,7 +901,7 @@ git commit -m "feat: add setup wizard to webpack build and extension manifest"
- [ ] **Step 1: Rebuild WASM**
```bash
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
```
- [ ] **Step 2: Rebuild extension**
@@ -927,7 +927,7 @@ Expected: All tests pass (including the new `embed_then_extract_round_trip`).
- Step 2: enter real host/token/repo, test connection works
- Step 3: pick a JPEG, enter passphrase, create vault pushes files
- Step 4: download reference image works, extension detection works
4. Verify the vault repo now has `.idfoto/salt`, `.idfoto/params.json`, `.idfoto/devices.json`, `manifest.enc`
4. Verify the vault repo now has `.relicario/salt`, `.relicario/params.json`, `.relicario/devices.json`, `manifest.enc`
5. Open extension popup, unlock with passphrase — should work with the just-created vault
- [ ] **Step 5: Fix any issues found**

View File

@@ -1,14 +1,14 @@
# idfoto WASM + Chrome MV3 Extension Implementation Plan
# relicario WASM + Chrome MV3 Extension 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:** Compile `idfoto-core` to WASM and wrap it in a Chrome MV3 browser extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access.
**Goal:** Compile `relicario-core` to WASM and wrap it in a Chrome MV3 browser extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access.
**Architecture:** Monolith service worker loads the WASM module and holds all state (master_key, cached manifest). Popup and content script are thin UI layers communicating via `chrome.runtime.sendMessage`. Vault data is fetched/committed directly via Gitea/GitHub REST APIs — no local clone, no CLI dependency.
**Tech Stack:** Rust + wasm-bindgen (WASM crate), TypeScript + webpack (extension), Chrome MV3 APIs
**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-wasm-extension-design.md`
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-wasm-extension-design.md`
---
@@ -17,7 +17,7 @@
### Rust (new crate)
```
crates/idfoto-wasm/
crates/relicario-wasm/
├── Cargo.toml
└── src/
└── lib.rs # wasm-bindgen wrappers + TOTP implementation
@@ -26,8 +26,8 @@ crates/idfoto-wasm/
### Rust (modified)
```
crates/idfoto-core/src/entry.rs # Add group field to Entry and ManifestEntry
Cargo.toml # Add idfoto-wasm to workspace members
crates/relicario-core/src/entry.rs # Add group field to Entry and ManifestEntry
Cargo.toml # Add relicario-wasm to workspace members
```
### Extension (all new)
@@ -73,13 +73,13 @@ extension/
## Task 0: Add Heavy Comments to Existing Rust Code
**Files:**
- Modify: `crates/idfoto-core/src/lib.rs`
- Modify: `crates/idfoto-core/src/error.rs`
- Modify: `crates/idfoto-core/src/crypto.rs`
- Modify: `crates/idfoto-core/src/entry.rs`
- Modify: `crates/idfoto-core/src/vault.rs`
- Modify: `crates/idfoto-core/src/imgsecret.rs`
- Modify: `crates/idfoto-cli/src/main.rs`
- Modify: `crates/relicario-core/src/lib.rs`
- Modify: `crates/relicario-core/src/error.rs`
- Modify: `crates/relicario-core/src/crypto.rs`
- Modify: `crates/relicario-core/src/entry.rs`
- Modify: `crates/relicario-core/src/vault.rs`
- Modify: `crates/relicario-core/src/imgsecret.rs`
- Modify: `crates/relicario-cli/src/main.rs`
Add thorough documentation comments to all existing Rust code. Every public function, struct, field, constant, and non-trivial private function should have doc comments explaining what it does, why it exists, and any important constraints. Module-level docs should explain the module's role in the overall architecture.
@@ -96,9 +96,9 @@ Guidelines:
- [ ] **Step 1: Add module-level docs and comments to `lib.rs`**
```rust
//! # idfoto-core
//! # relicario-core
//!
//! Platform-agnostic core library for the idfoto password manager.
//! Platform-agnostic core library for the relicario password manager.
//!
//! This crate is deliberately bytes-in/bytes-out — no filesystem, no network,
//! no git operations. This makes it portable to WASM (browser extension),
@@ -174,7 +174,7 @@ Expected: All tests pass unchanged.
- [ ] **Step 9: Commit**
```bash
git add crates/idfoto-core/src/ crates/idfoto-cli/src/main.rs
git add crates/relicario-core/src/ crates/relicario-cli/src/main.rs
git commit -m "docs: add heavy documentation comments to all Rust code"
```
@@ -183,14 +183,14 @@ git commit -m "docs: add heavy documentation comments to all Rust code"
## Task 1: Add `group` Field to Core Data Model
**Files:**
- Modify: `crates/idfoto-core/src/entry.rs`
- Modify: `crates/idfoto-core/src/vault.rs` (test helpers)
- Modify: `crates/idfoto-cli/src/main.rs` (Entry construction sites)
- Test: `crates/idfoto-core/src/entry.rs` (inline tests)
- Modify: `crates/relicario-core/src/entry.rs`
- Modify: `crates/relicario-core/src/vault.rs` (test helpers)
- Modify: `crates/relicario-cli/src/main.rs` (Entry construction sites)
- Test: `crates/relicario-core/src/entry.rs` (inline tests)
- [ ] **Step 1: Add `group` field to `Entry` struct**
In `crates/idfoto-core/src/entry.rs`, add the field after `totp_secret`:
In `crates/relicario-core/src/entry.rs`, add the field after `totp_secret`:
```rust
pub struct Entry {
@@ -232,7 +232,7 @@ pub struct ManifestEntry {
Update every place that constructs `Entry` or `ManifestEntry` to include `group: None`. These are:
In `crates/idfoto-core/src/entry.rs` tests — `entry_serialization_round_trip`, `manifest_add_and_lookup`, `manifest_serialization_round_trip`, `manifest_search_case_insensitive`:
In `crates/relicario-core/src/entry.rs` tests — `entry_serialization_round_trip`, `manifest_add_and_lookup`, `manifest_serialization_round_trip`, `manifest_search_case_insensitive`:
```rust
// Every Entry construction gets:
@@ -242,7 +242,7 @@ group: None,
group: None,
```
In `crates/idfoto-core/src/vault.rs` tests — `sample_entry()` helper and `manifest_encrypt_decrypt_round_trip`:
In `crates/relicario-core/src/vault.rs` tests — `sample_entry()` helper and `manifest_encrypt_decrypt_round_trip`:
```rust
// sample_entry() gets:
@@ -252,7 +252,7 @@ group: None,
group: None,
```
In `crates/idfoto-core/tests/integration.rs``full_vault_workflow()` Entry construction (line ~55) and ManifestEntry (line ~101):
In `crates/relicario-core/tests/integration.rs``full_vault_workflow()` Entry construction (line ~55) and ManifestEntry (line ~101):
```rust
// Entry construction gets:
@@ -262,7 +262,7 @@ group: None,
group: None,
```
In `crates/idfoto-cli/src/main.rs``cmd_add()` Entry construction (line ~328), `cmd_add()` ManifestEntry (line ~349), `cmd_edit()` Entry construction (line ~513), `cmd_edit()` ManifestEntry (line ~536):
In `crates/relicario-cli/src/main.rs``cmd_add()` Entry construction (line ~328), `cmd_add()` ManifestEntry (line ~349), `cmd_edit()` Entry construction (line ~513), `cmd_edit()` ManifestEntry (line ~536):
```rust
// Every Entry construction gets:
@@ -274,7 +274,7 @@ group: None,
- [ ] **Step 4: Add a test for backwards compatibility (deserialize without group)**
In `crates/idfoto-core/src/entry.rs` tests:
In `crates/relicario-core/src/entry.rs` tests:
```rust
#[test]
@@ -329,35 +329,35 @@ Expected: All tests pass, including new backwards-compatibility tests.
- [ ] **Step 6: Commit**
```bash
git add crates/idfoto-core/src/entry.rs crates/idfoto-core/src/vault.rs crates/idfoto-core/tests/integration.rs crates/idfoto-cli/src/main.rs
git add crates/relicario-core/src/entry.rs crates/relicario-core/src/vault.rs crates/relicario-core/tests/integration.rs crates/relicario-cli/src/main.rs
git commit -m "feat: add group field to Entry and ManifestEntry"
```
---
## Task 2: Create `idfoto-wasm` Crate
## Task 2: Create `relicario-wasm` Crate
**Files:**
- Create: `crates/idfoto-wasm/Cargo.toml`
- Create: `crates/idfoto-wasm/src/lib.rs`
- Create: `crates/relicario-wasm/Cargo.toml`
- Create: `crates/relicario-wasm/src/lib.rs`
- Modify: `Cargo.toml` (workspace members)
- [ ] **Step 1: Create Cargo.toml**
Create `crates/idfoto-wasm/Cargo.toml`:
Create `crates/relicario-wasm/Cargo.toml`:
```toml
[package]
name = "idfoto-wasm"
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"]
[dependencies]
idfoto-core = { path = "../idfoto-core" }
relicario-core = { path = "../relicario-core" }
wasm-bindgen = "0.2"
js-sys = "0.3"
serde_json = "1"
@@ -371,21 +371,21 @@ wasm-bindgen-test = "0.3"
- [ ] **Step 2: Add to workspace**
In root `Cargo.toml`, add `"crates/idfoto-wasm"` to the members list:
In root `Cargo.toml`, add `"crates/relicario-wasm"` to the members list:
```toml
[workspace]
resolver = "2"
members = [
"crates/idfoto-core",
"crates/idfoto-cli",
"crates/idfoto-wasm",
"crates/relicario-core",
"crates/relicario-cli",
"crates/relicario-wasm",
]
```
- [ ] **Step 3: Write the WASM wrapper**
Create `crates/idfoto-wasm/src/lib.rs`:
Create `crates/relicario-wasm/src/lib.rs`:
```rust
use wasm_bindgen::prelude::*;
@@ -399,7 +399,7 @@ pub fn derive_master_key(
salt: &[u8],
params_json: &str,
) -> Result<Vec<u8>, JsValue> {
let params: idfoto_core::KdfParams =
let params: relicario_core::KdfParams =
serde_json::from_str(params_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
let image_secret: [u8; 32] = image_secret
@@ -409,7 +409,7 @@ pub fn derive_master_key(
.try_into()
.map_err(|_| JsValue::from_str("salt must be exactly 32 bytes"))?;
let key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
let key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(key.to_vec())
@@ -421,7 +421,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
idfoto_core::crypto::encrypt(&key, plaintext).map_err(|e| JsValue::from_str(&e.to_string()))
relicario_core::crypto::encrypt(&key, plaintext).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Decrypt ciphertext with a 32-byte key. Returns plaintext bytes.
@@ -430,14 +430,14 @@ pub fn decrypt(ciphertext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
idfoto_core::crypto::decrypt(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))
relicario_core::crypto::decrypt(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Extract a 256-bit secret from a JPEG with an embedded secret.
#[wasm_bindgen]
pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
let secret =
idfoto_core::imgsecret::extract(jpeg_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
relicario_core::imgsecret::extract(jpeg_bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(secret.to_vec())
}
@@ -447,9 +447,9 @@ pub fn encrypt_entry(entry_json: &str, key: &[u8]) -> Result<Vec<u8>, JsValue> {
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
let entry: idfoto_core::Entry =
let entry: relicario_core::Entry =
serde_json::from_str(entry_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
idfoto_core::encrypt_entry(&key, &entry).map_err(|e| JsValue::from_str(&e.to_string()))
relicario_core::encrypt_entry(&key, &entry).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Decrypt an entry from encrypted bytes. Returns JSON string.
@@ -459,7 +459,7 @@ pub fn decrypt_entry(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue> {
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
let entry =
idfoto_core::decrypt_entry(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))?;
relicario_core::decrypt_entry(&key, ciphertext).map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&entry).map_err(|e| JsValue::from_str(&e.to_string()))
}
@@ -469,9 +469,9 @@ pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsVa
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
let manifest: idfoto_core::Manifest =
let manifest: relicario_core::Manifest =
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
idfoto_core::encrypt_manifest(&key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
relicario_core::encrypt_manifest(&key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Decrypt a manifest from encrypted bytes. Returns JSON string.
@@ -480,7 +480,7 @@ pub fn decrypt_manifest(ciphertext: &[u8], key: &[u8]) -> Result<String, JsValue
let key: [u8; 32] = key
.try_into()
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
let manifest = idfoto_core::decrypt_manifest(&key, ciphertext)
let manifest = relicario_core::decrypt_manifest(&key, ciphertext)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&manifest).map_err(|e| JsValue::from_str(&e.to_string()))
}
@@ -618,27 +618,27 @@ mod tests {
- [ ] **Step 4: Verify it compiles**
Run: `cargo build -p idfoto-wasm`
Run: `cargo build -p relicario-wasm`
Expected: Compiles successfully.
- [ ] **Step 5: Run tests**
Run: `cargo test -p idfoto-wasm`
Run: `cargo test -p relicario-wasm`
Expected: All tests pass, including TOTP RFC 6238 test vectors.
- [ ] **Step 6: Test WASM compilation**
Run: `cargo install wasm-pack` (if not already installed), then:
```bash
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
```
Expected: Produces `extension/wasm/idfoto_wasm.js` and `extension/wasm/idfoto_wasm_bg.wasm`. Note the WASM binary size for later reference.
Expected: Produces `extension/wasm/relicario_wasm.js` and `extension/wasm/relicario_wasm_bg.wasm`. Note the WASM binary size for later reference.
- [ ] **Step 7: Commit**
```bash
git add crates/idfoto-wasm/ Cargo.toml extension/wasm/
git commit -m "feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP"
git add crates/relicario-wasm/ Cargo.toml extension/wasm/
git commit -m "feat: add relicario-wasm crate with wasm-bindgen wrappers and TOTP"
```
---
@@ -658,13 +658,13 @@ git commit -m "feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP"
```json
{
"name": "idfoto-extension",
"name": "relicario-extension",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --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",
"build:all": "npm run build:wasm && npm run build"
},
"devDependencies": {
@@ -682,13 +682,13 @@ Note: `@anthropic-ai/sdk` is NOT needed — remove that. The devDependencies sho
```json
{
"name": "idfoto-extension",
"name": "relicario-extension",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --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",
"build:all": "npm run build:wasm && npm run build"
},
"devDependencies": {
@@ -706,13 +706,13 @@ Actually, strike that — no anthropic SDK. Final version:
```json
{
"name": "idfoto-extension",
"name": "relicario-extension",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --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",
"build:all": "npm run build:wasm && npm run build"
},
"devDependencies": {
@@ -781,8 +781,8 @@ module.exports = {
{ from: 'src/popup/index.html', to: 'popup.html' },
{ from: 'src/popup/styles.css', to: 'styles.css' },
{ 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: '.' },
],
}),
],
@@ -797,7 +797,7 @@ module.exports = {
```json
{
"manifest_version": 3,
"name": "idfoto",
"name": "relicario",
"version": "0.1.0",
"description": "Two-factor encrypted password manager",
"permissions": ["storage", "activeTab", "clipboardWrite"],
@@ -838,7 +838,7 @@ Create `extension/src/popup/index.html`:
<meta charset="UTF-8">
<meta name="viewport" content="width=360">
<link rel="stylesheet" href="styles.css">
<title>idfoto</title>
<title>relicario</title>
</head>
<body>
<div id="app"></div>
@@ -1286,15 +1286,15 @@ import type { GitHost } from './git-host';
import type { Entry, Manifest, ManifestEntry } from '../shared/types';
// These will be set by the service worker index after WASM init
let wasm: typeof import('../../wasm/idfoto_wasm');
let wasm: typeof import('../../wasm/relicario_wasm');
export function setWasm(w: typeof wasm) {
wasm = w;
}
export async function fetchVaultMeta(git: GitHost): Promise<{ salt: Uint8Array; paramsJson: string }> {
const salt = await git.readFile('.idfoto/salt');
const paramsBytes = await git.readFile('.idfoto/params.json');
const salt = await git.readFile('.relicario/salt');
const paramsBytes = await git.readFile('.relicario/params.json');
const paramsJson = new TextDecoder().decode(paramsBytes);
return { salt, paramsJson };
}
@@ -1414,13 +1414,13 @@ import {
let masterKey: Uint8Array | null = null;
let manifest: Manifest | null = null;
let gitHost: GitHost | null = null;
let wasm: typeof import('../../wasm/idfoto_wasm') | null = null;
let wasm: typeof import('../../wasm/relicario_wasm') | null = null;
// ─── WASM initialization ───────────────────────────────────────────────────
async function initWasm(): Promise<typeof import('../../wasm/idfoto_wasm')> {
async function initWasm(): Promise<typeof import('../../wasm/relicario_wasm')> {
if (wasm) return wasm;
const mod = await import(/* webpackIgnore: true */ './idfoto_wasm.js');
const mod = await import(/* webpackIgnore: true */ './relicario_wasm.js');
await mod.default();
wasm = mod;
setWasm(mod);
@@ -2118,7 +2118,7 @@ import { sendMessage, navigate } from '../popup';
export function renderUnlock(container: HTMLElement) {
container.innerHTML = `
<div class="brand">idfoto</div>
<div class="brand">relicario</div>
<div style="margin-top: 16px">
<div class="label">PASSPHRASE</div>
<input type="password" id="passphrase" placeholder="Enter passphrase..." autofocus>
@@ -2181,7 +2181,7 @@ import type { ManifestEntry } from '../../shared/types';
export async function renderEntryList(container: HTMLElement) {
container.innerHTML = `
<div class="header">
<div class="brand">idfoto</div>
<div class="brand">relicario</div>
<div class="status">🔓 unlocked</div>
</div>
<div class="search-bar">
@@ -2702,7 +2702,7 @@ export function renderSetupWizard(container: HTMLElement) {
function render() {
container.innerHTML = `
<div class="brand">idfoto setup</div>
<div class="brand">relicario setup</div>
<div class="wizard-step">step ${step} of 3 — ${['repository', 'reference image', 'test unlock'][step - 1]}</div>
<div class="progress-bar"><div class="progress-bar-fill" style="width: ${(step / 3) * 100}%"></div></div>
<div id="wizard-content"></div>
@@ -3128,10 +3128,10 @@ function showPicker(
passwordField: HTMLInputElement
) {
// Remove any existing picker
document.querySelectorAll('.idfoto-picker').forEach((el) => el.remove());
document.querySelectorAll('.relicario-picker').forEach((el) => el.remove());
const picker = document.createElement('div');
picker.className = 'idfoto-picker';
picker.className = 'relicario-picker';
picker.style.cssText = `
position: absolute;
right: 0;
@@ -3222,7 +3222,7 @@ git commit -m "feat: add content script with form detection, field icon, and aut
```bash
# Build WASM
wasm-pack build crates/idfoto-wasm --target web --out-dir ../../extension/wasm
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
# Install deps and build extension
cd extension && npm install && npm run build
@@ -3233,8 +3233,8 @@ Expected: `extension/dist/` contains all files needed to load as an unpacked Chr
- [ ] **Step 2: Note the WASM binary size**
```bash
ls -lh extension/wasm/idfoto_wasm_bg.wasm
ls -lh extension/dist/idfoto_wasm_bg.wasm
ls -lh extension/wasm/relicario_wasm_bg.wasm
ls -lh extension/dist/relicario_wasm_bg.wasm
```
Record the size for reference. If >2 MB uncompressed, consider optimizing later.
@@ -3276,7 +3276,7 @@ git commit -m "feat: complete WASM + Chrome MV3 extension build"
|------|-------------|--------------|
| 0 | Add heavy comments to existing Rust code | None |
| 1 | Add `group` field to core data model | Task 0 |
| 2 | Create `idfoto-wasm` crate | Task 1 |
| 2 | Create `relicario-wasm` crate | Task 1 |
| 3 | Extension scaffolding | Task 2 |
| 4 | Shared types and messages | Task 3 |
| 5 | Git API layer | Task 4 |