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:
18
CLAUDE.md
18
CLAUDE.md
@@ -1,15 +1,15 @@
|
|||||||
# CLAUDE.md — idfoto
|
# CLAUDE.md — relicario
|
||||||
|
|
||||||
## What is this
|
## What is this
|
||||||
|
|
||||||
idfoto is a git-backed, self-hostable password manager with a Rust core. Two-factor vault decryption: passphrase + a reference JPEG carrying a 256-bit secret embedded via DCT steganography. The server only ever sees opaque ciphertext.
|
relicario is a git-backed, self-hostable password manager with a Rust core. Two-factor vault decryption: passphrase + a reference JPEG carrying a 256-bit secret embedded via DCT steganography. The server only ever sees opaque ciphertext.
|
||||||
|
|
||||||
## Build and test
|
## Build and test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build # build everything
|
cargo build # build everything
|
||||||
cargo test # run all tests (unit + integration)
|
cargo test # run all tests (unit + integration)
|
||||||
cargo test -p idfoto-core # core library tests only
|
cargo test -p relicario-core # core library tests only
|
||||||
cargo run -- --help # CLI help
|
cargo run -- --help # CLI help
|
||||||
cargo run -- generate -l 32 # quick smoke test
|
cargo run -- generate -l 32 # quick smoke test
|
||||||
```
|
```
|
||||||
@@ -18,24 +18,24 @@ cargo run -- generate -l 32 # quick smoke test
|
|||||||
|
|
||||||
```
|
```
|
||||||
crates/
|
crates/
|
||||||
├── idfoto-core/ # Platform-agnostic library (no filesystem, no git, no network)
|
├── relicario-core/ # Platform-agnostic library (no filesystem, no git, no network)
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── lib.rs # Re-exports public API
|
│ │ ├── lib.rs # Re-exports public API
|
||||||
│ │ ├── error.rs # IdfotoError enum (thiserror)
|
│ │ ├── error.rs # RelicarioError enum (thiserror)
|
||||||
│ │ ├── crypto.rs # Argon2id KDF + XChaCha20-Poly1305 encrypt/decrypt
|
│ │ ├── crypto.rs # Argon2id KDF + XChaCha20-Poly1305 encrypt/decrypt
|
||||||
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs (serde)
|
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs (serde)
|
||||||
│ │ ├── vault.rs # encrypt_entry, decrypt_entry, encrypt_manifest, decrypt_manifest
|
│ │ ├── vault.rs # encrypt_entry, decrypt_entry, encrypt_manifest, decrypt_manifest
|
||||||
│ │ └── imgsecret.rs # DCT-based 256-bit secret embedding in JPEGs
|
│ │ └── imgsecret.rs # DCT-based 256-bit secret embedding in JPEGs
|
||||||
│ └── tests/
|
│ └── tests/
|
||||||
│ └── integration.rs # Full-workflow and two-factor independence tests
|
│ └── integration.rs # Full-workflow and two-factor independence tests
|
||||||
└── idfoto-cli/ # CLI binary
|
└── relicario-cli/ # CLI binary
|
||||||
└── src/
|
└── src/
|
||||||
└── main.rs # clap CLI: init, add, get, list, edit, rm, sync, generate, device
|
└── main.rs # clap CLI: init, add, get, list, edit, rm, sync, generate, device
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key design decisions
|
## Key design decisions
|
||||||
|
|
||||||
- **idfoto-core is bytes-in/bytes-out.** No filesystem, no network, no git operations. Makes it portable to WASM, Android, iOS.
|
- **relicario-core is bytes-in/bytes-out.** No filesystem, no network, no git operations. Makes it portable to WASM, Android, iOS.
|
||||||
- **XChaCha20-Poly1305** over AES-GCM — 192-bit nonce eliminates collision risk, fast in WASM/ARM without AES-NI.
|
- **XChaCha20-Poly1305** over AES-GCM — 192-bit nonce eliminates collision risk, fast in WASM/ARM without AES-NI.
|
||||||
- **Single master_key** (no per-entry subkeys) — simpler, sufficient for family vault sizes.
|
- **Single master_key** (no per-entry subkeys) — simpler, sufficient for family vault sizes.
|
||||||
- **imgsecret uses central-embed DCT** — embeds only in the middle 70% of the image (15% crumple zone for crop tolerance), with majority voting across 5-50 redundant copies.
|
- **imgsecret uses central-embed DCT** — embeds only in the middle 70% of the image (15% crumple zone for crop tolerance), with majority voting across 5-50 redundant copies.
|
||||||
@@ -62,11 +62,11 @@ passphrase (UTF-8 bytes) || image_secret (32 bytes from reference JPEG)
|
|||||||
|
|
||||||
## Remote
|
## Remote
|
||||||
|
|
||||||
Source code: `ssh://git@git.adlee.work:2222/alee/idfoto.git`
|
Source code: `ssh://git@git.adlee.work:2222/alee/relicario.git`
|
||||||
|
|
||||||
## Design spec
|
## Design spec
|
||||||
|
|
||||||
Full threat model, entropy analysis, and architecture: `docs/superpowers/specs/2026-04-11-idfoto-design.md`
|
Full threat model, entropy analysis, and architecture: `docs/superpowers/specs/2026-04-11-relicario-design.md`
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
96
Cargo.lock
generated
96
Cargo.lock
generated
@@ -590,54 +590,6 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idfoto-cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"arboard",
|
|
||||||
"clap",
|
|
||||||
"dirs",
|
|
||||||
"ed25519-dalek",
|
|
||||||
"hex",
|
|
||||||
"idfoto-core",
|
|
||||||
"rand",
|
|
||||||
"rpassword",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idfoto-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"argon2",
|
|
||||||
"chacha20poly1305",
|
|
||||||
"ed25519-dalek",
|
|
||||||
"image",
|
|
||||||
"rand",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idfoto-wasm"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"data-encoding",
|
|
||||||
"getrandom",
|
|
||||||
"hmac",
|
|
||||||
"idfoto-core",
|
|
||||||
"image",
|
|
||||||
"js-sys",
|
|
||||||
"serde_json",
|
|
||||||
"sha1",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-test",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.10"
|
version = "0.25.10"
|
||||||
@@ -1056,6 +1008,54 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relicario-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"arboard",
|
||||||
|
"clap",
|
||||||
|
"dirs",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"hex",
|
||||||
|
"rand",
|
||||||
|
"relicario-core",
|
||||||
|
"rpassword",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relicario-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
|
"chacha20poly1305",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"image",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "relicario-wasm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"data-encoding",
|
||||||
|
"getrandom",
|
||||||
|
"hmac",
|
||||||
|
"image",
|
||||||
|
"js-sys",
|
||||||
|
"relicario-core",
|
||||||
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-test",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpassword"
|
name = "rpassword"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/idfoto-core",
|
"crates/relicario-core",
|
||||||
"crates/idfoto-cli",
|
"crates/relicario-cli",
|
||||||
"crates/idfoto-wasm",
|
"crates/relicario-wasm",
|
||||||
]
|
]
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -1,8 +1,8 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="extension/icons/idfoto-logo.svg" alt="idfoto" width="128" height="128">
|
<img src="extension/icons/relicario-logo.svg" alt="relicario" width="128" height="128">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# idfoto
|
# 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.
|
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.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ Your reference photo (something you have)
|
|||||||
your device (opaque ciphertext)
|
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.
|
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.
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ To unlock the vault, you provide your passphrase and point the client at the ref
|
|||||||
A git repository containing:
|
A git repository containing:
|
||||||
- `manifest.enc` — opaque binary blob
|
- `manifest.enc` — opaque binary blob
|
||||||
- `entries/*.enc` — more opaque binary blobs
|
- `entries/*.enc` — more opaque binary blobs
|
||||||
- `.idfoto/salt` — a random 32-byte value (not secret)
|
- `.relicario/salt` — a random 32-byte value (not secret)
|
||||||
- `.idfoto/params.json` — Argon2id parameters (not secret)
|
- `.relicario/params.json` — Argon2id parameters (not secret)
|
||||||
- `.idfoto/devices.json` — authorized device public keys
|
- `.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.
|
That's it. No plaintext. No metadata about what's inside. No keys, no passphrases, no reference images.
|
||||||
|
|
||||||
@@ -58,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 |
|
| LastPass | ~40-60 bits (master password only) | 1 |
|
||||||
| Bitwarden | ~40-60 bits (master password only) | 1 |
|
| Bitwarden | ~40-60 bits (master password only) | 1 |
|
||||||
| 1Password | password + 128-bit Secret Key | 2 |
|
| 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
|
### What we don't protect against
|
||||||
|
|
||||||
@@ -73,31 +73,31 @@ No single point of failure. The two-factor design means the passphrase alone can
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Create a vault (pick any JPEG as the carrier)
|
# 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
|
# Add a credential
|
||||||
idfoto add
|
relicario add
|
||||||
|
|
||||||
# Retrieve it
|
# Retrieve it
|
||||||
idfoto get github
|
relicario get github
|
||||||
|
|
||||||
# List everything
|
# List everything
|
||||||
idfoto list
|
relicario list
|
||||||
|
|
||||||
# Sync with your git remote
|
# Sync with your git remote
|
||||||
idfoto sync
|
relicario sync
|
||||||
|
|
||||||
# Generate a random password
|
# Generate a random password
|
||||||
idfoto generate -l 32
|
relicario generate -l 32
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variable
|
### 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 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:
|
The embedding survives:
|
||||||
- JPEG recompression (tested down to quality 85)
|
- JPEG recompression (tested down to quality 85)
|
||||||
@@ -109,20 +109,20 @@ This means your reference image can live on your Instagram, your personal websit
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
idfoto/
|
relicario/
|
||||||
├── crates/
|
├── 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
|
│ │ ├── crypto.rs # Argon2id KDF + XChaCha20-Poly1305 AEAD
|
||||||
│ │ ├── imgsecret.rs # DCT steganography: embed/extract 256-bit secrets in JPEGs
|
│ │ ├── imgsecret.rs # DCT steganography: embed/extract 256-bit secrets in JPEGs
|
||||||
│ │ ├── entry.rs # Entry, Manifest data model (serde)
|
│ │ ├── entry.rs # Entry, Manifest data model (serde)
|
||||||
│ │ └── vault.rs # Encrypt/decrypt entries and manifests
|
│ │ └── 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/
|
└── docs/
|
||||||
└── superpowers/
|
└── superpowers/
|
||||||
└── specs/ # Design specification with full threat model
|
└── 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
|
### Crypto primitives
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ my-vault.git/
|
|||||||
├── entries/
|
├── entries/
|
||||||
│ ├── a1b2c3d4.enc # One encrypted entry per file
|
│ ├── a1b2c3d4.enc # One encrypted entry per file
|
||||||
│ └── e5f6a7b8.enc
|
│ └── e5f6a7b8.enc
|
||||||
└── .idfoto/
|
└── .relicario/
|
||||||
├── salt # 32-byte random salt (not secret)
|
├── salt # 32-byte random salt (not secret)
|
||||||
├── params.json # KDF parameters
|
├── params.json # KDF parameters
|
||||||
└── devices.json # Authorized device public keys
|
└── devices.json # Authorized device public keys
|
||||||
@@ -158,14 +158,14 @@ Entry IDs are random hex strings. Git history is preserved — every add/edit/de
|
|||||||
|
|
||||||
## Device management
|
## 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.
|
Revoking a device: remove its key from `devices.json` and commit. No passphrase or reference image rotation needed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
idfoto device add --name laptop
|
relicario device add --name laptop
|
||||||
idfoto device list
|
relicario device list
|
||||||
idfoto device revoke laptop
|
relicario device revoke laptop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -173,20 +173,20 @@ idfoto device revoke laptop
|
|||||||
Requires Rust stable (1.70+).
|
Requires Rust stable (1.70+).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone ssh://git@git.adlee.work:2222/alee/idfoto.git
|
git clone ssh://git@git.adlee.work:2222/alee/relicario.git
|
||||||
cd idfoto
|
cd relicario
|
||||||
cargo build --release
|
cargo build --release
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary is at `target/release/idfoto`.
|
The binary is at `target/release/relicario`.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] WASM build + Chrome browser extension (inline crypto, no native messaging)
|
- [ ] WASM build + Chrome browser extension (inline crypto, no native messaging)
|
||||||
- [ ] Secure notes (free-form encrypted text entries)
|
- [ ] Secure notes (free-form encrypted text entries)
|
||||||
- [ ] Secure document storage (encrypted file attachments up to 5-10 MB)
|
- [ ] 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)
|
- [ ] Android/iOS clients (Rust core compiles to ARM)
|
||||||
- [ ] Import from LastPass/Bitwarden/1Password
|
- [ ] Import from LastPass/Bitwarden/1Password
|
||||||
- [ ] Firefox/Safari extensions
|
- [ ] Firefox/Safari extensions
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "idfoto-cli"
|
name = "relicario-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "CLI for idfoto password manager"
|
description = "CLI for relicario password manager"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "idfoto"
|
name = "relicario"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idfoto-core = { path = "../idfoto-core" }
|
relicario-core = { path = "../relicario-core" }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
rpassword = "5"
|
rpassword = "5"
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
//! 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
|
//! This binary provides the filesystem, git, and terminal I/O that
|
||||||
//! [`idfoto_core`] intentionally excludes. It is the "glue" between the
|
//! [`relicario_core`] intentionally excludes. It is the "glue" between the
|
||||||
//! platform-agnostic core library and the user's local environment.
|
//! platform-agnostic core library and the user's local environment.
|
||||||
//!
|
//!
|
||||||
//! ## Vault layout on disk
|
//! ## Vault layout on disk
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! <vault_dir>/
|
//! <vault_dir>/
|
||||||
//! .idfoto/
|
//! .relicario/
|
||||||
//! salt # 32-byte random salt for Argon2id KDF
|
//! salt # 32-byte random salt for Argon2id KDF
|
||||||
//! params.json # KDF tuning parameters (m, t, p)
|
//! params.json # KDF tuning parameters (m, t, p)
|
||||||
//! devices.json # registered device public keys
|
//! devices.json # registered device public keys
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Every command that accesses vault data follows this sequence:
|
//! Every command that accesses vault data follows this sequence:
|
||||||
//!
|
//!
|
||||||
//! 1. Locate the reference image (via `IDFOTO_IMAGE` env var or interactive prompt).
|
//! 1. Locate the reference image (via `RELICARIO_IMAGE` env var or interactive prompt).
|
||||||
//! 2. Prompt for the passphrase (read from stderr, not echoed).
|
//! 2. Prompt for the passphrase (read from stderr, not echoed).
|
||||||
//! 3. Extract the 32-byte image secret from the reference JPEG via DCT steganography.
|
//! 3. Extract the 32-byte image secret from the reference JPEG via DCT steganography.
|
||||||
//! 4. Read the vault salt and KDF params from `.idfoto/`.
|
//! 4. Read the vault salt and KDF params from `.relicario/`.
|
||||||
//! 5. Derive the master key: `Argon2id(passphrase || image_secret, salt, params)`.
|
//! 5. Derive the master key: `Argon2id(passphrase || image_secret, salt, params)`.
|
||||||
//! 6. Use the master key to decrypt the manifest and/or individual entries.
|
//! 6. Use the master key to decrypt the manifest and/or individual entries.
|
||||||
//!
|
//!
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use idfoto_core::{
|
use relicario_core::{
|
||||||
decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest, generate_entry_id,
|
decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest, generate_entry_id,
|
||||||
Entry, KdfParams, Manifest, ManifestEntry,
|
Entry, KdfParams, Manifest, ManifestEntry,
|
||||||
};
|
};
|
||||||
@@ -56,7 +56,7 @@ use std::process::Command;
|
|||||||
/// Top-level CLI argument parser.
|
/// Top-level CLI argument parser.
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "idfoto",
|
name = "relicario",
|
||||||
version,
|
version,
|
||||||
about = "Git-backed password manager with reference image authentication"
|
about = "Git-backed password manager with reference image authentication"
|
||||||
)]
|
)]
|
||||||
@@ -68,7 +68,7 @@ struct Cli {
|
|||||||
/// All available CLI subcommands.
|
/// All available CLI subcommands.
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
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,
|
/// Creates the directory structure, generates a random image secret,
|
||||||
/// embeds it in the carrier image, and sets up git.
|
/// embeds it in the carrier image, and sets up git.
|
||||||
Init {
|
Init {
|
||||||
@@ -132,7 +132,7 @@ enum DeviceCommands {
|
|||||||
|
|
||||||
// ─── Device entry ───────────────────────────────────────────────────────────
|
// ─── Device entry ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// A registered device, stored in `.idfoto/devices.json`.
|
/// A registered device, stored in `.relicario/devices.json`.
|
||||||
///
|
///
|
||||||
/// Each device has an ed25519 keypair. The private key lives on the device
|
/// Each device has an ed25519 keypair. The private key lives on the device
|
||||||
/// itself (in the user's config directory); only the public key is stored
|
/// itself (in the user's config directory); only the public key is stored
|
||||||
@@ -149,23 +149,23 @@ struct DeviceEntry {
|
|||||||
// ─── Helper functions ───────────────────────────────────────────────────────
|
// ─── Helper functions ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Returns the vault root directory (the current working directory).
|
/// 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 {
|
fn vault_dir() -> PathBuf {
|
||||||
std::env::current_dir().expect("failed to get current directory")
|
std::env::current_dir().expect("failed to get current directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the `.idfoto/` configuration directory within the vault.
|
/// Returns the path to the `.relicario/` configuration directory within the vault.
|
||||||
fn idfoto_dir() -> PathBuf {
|
fn relicario_dir() -> PathBuf {
|
||||||
vault_dir().join(".idfoto")
|
vault_dir().join(".relicario")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the 32-byte vault salt from `.idfoto/salt`.
|
/// Read the 32-byte vault salt from `.relicario/salt`.
|
||||||
///
|
///
|
||||||
/// The salt is generated once during `init` and is unique per vault. It is
|
/// The salt is generated once during `init` and is unique per vault. It is
|
||||||
/// not secret (stored in plaintext) -- its purpose is to prevent precomputed
|
/// not secret (stored in plaintext) -- its purpose is to prevent precomputed
|
||||||
/// rainbow table attacks against the Argon2id KDF.
|
/// rainbow table attacks against the Argon2id KDF.
|
||||||
fn read_salt() -> Result<[u8; 32]> {
|
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];
|
let mut salt = [0u8; 32];
|
||||||
if data.len() != 32 {
|
if data.len() != 32 {
|
||||||
bail!("invalid salt file: expected 32 bytes, got {}", data.len());
|
bail!("invalid salt file: expected 32 bytes, got {}", data.len());
|
||||||
@@ -174,9 +174,9 @@ fn read_salt() -> Result<[u8; 32]> {
|
|||||||
Ok(salt)
|
Ok(salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the KDF parameters from `.idfoto/params.json`.
|
/// Read the KDF parameters from `.relicario/params.json`.
|
||||||
fn read_params() -> Result<KdfParams> {
|
fn read_params() -> Result<KdfParams> {
|
||||||
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")?;
|
.context("failed to read params.json")?;
|
||||||
let params: KdfParams = serde_json::from_str(&data).context("failed to parse params.json")?;
|
let params: KdfParams = serde_json::from_str(&data).context("failed to parse params.json")?;
|
||||||
Ok(params)
|
Ok(params)
|
||||||
@@ -184,10 +184,10 @@ fn read_params() -> Result<KdfParams> {
|
|||||||
|
|
||||||
/// Locate the reference image path.
|
/// Locate the reference image path.
|
||||||
///
|
///
|
||||||
/// First checks the `IDFOTO_IMAGE` environment variable (useful for scripting
|
/// First checks the `RELICARIO_IMAGE` environment variable (useful for scripting
|
||||||
/// and testing). If not set, prompts the user interactively.
|
/// and testing). If not set, prompts the user interactively.
|
||||||
fn get_image_path() -> Result<PathBuf> {
|
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));
|
return Ok(PathBuf::from(path));
|
||||||
}
|
}
|
||||||
let path = prompt("Reference image path")?;
|
let path = prompt("Reference image path")?;
|
||||||
@@ -206,12 +206,12 @@ fn unlock(image_path: &PathBuf) -> Result<[u8; 32]> {
|
|||||||
|
|
||||||
let jpeg_data = fs::read(image_path).context("failed to read reference image")?;
|
let jpeg_data = fs::read(image_path).context("failed to read reference image")?;
|
||||||
let image_secret =
|
let image_secret =
|
||||||
idfoto_core::imgsecret::extract(&jpeg_data).context("failed to extract image secret")?;
|
relicario_core::imgsecret::extract(&jpeg_data).context("failed to extract image secret")?;
|
||||||
|
|
||||||
let salt = read_salt()?;
|
let salt = read_salt()?;
|
||||||
let params = read_params()?;
|
let params = read_params()?;
|
||||||
|
|
||||||
let master_key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||||
.context("failed to derive master key")?;
|
.context("failed to derive master key")?;
|
||||||
|
|
||||||
Ok(master_key)
|
Ok(master_key)
|
||||||
@@ -318,7 +318,7 @@ fn generate_password(length: usize) -> String {
|
|||||||
|
|
||||||
// ─── Command implementations ────────────────────────────────────────────────
|
// ─── Command implementations ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Initialize a new idfoto vault in the current directory.
|
/// Initialize a new relicario vault in the current directory.
|
||||||
///
|
///
|
||||||
/// Full sequence:
|
/// Full sequence:
|
||||||
/// 1. Read the carrier JPEG provided by the user.
|
/// 1. Read the carrier JPEG provided by the user.
|
||||||
@@ -328,7 +328,7 @@ fn generate_password(length: usize) -> String {
|
|||||||
/// 5. Prompt for a passphrase (minimum 8 characters, with confirmation).
|
/// 5. Prompt for a passphrase (minimum 8 characters, with confirmation).
|
||||||
/// 6. Generate a random 32-byte salt.
|
/// 6. Generate a random 32-byte salt.
|
||||||
/// 7. Derive the master key from passphrase + image_secret + salt.
|
/// 7. Derive the master key from passphrase + image_secret + salt.
|
||||||
/// 8. Create the vault directory structure (.idfoto/, entries/).
|
/// 8. Create the vault directory structure (.relicario/, entries/).
|
||||||
/// 9. Write salt, KDF params, empty devices list, and encrypted empty manifest.
|
/// 9. Write salt, KDF params, empty devices list, and encrypted empty manifest.
|
||||||
/// 10. Initialize git and create the first commit.
|
/// 10. Initialize git and create the first commit.
|
||||||
fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||||
@@ -341,7 +341,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
|||||||
|
|
||||||
// 3. Embed secret into carrier
|
// 3. Embed secret into carrier
|
||||||
let reference_jpeg =
|
let reference_jpeg =
|
||||||
idfoto_core::imgsecret::embed(&carrier, &image_secret).context("failed to embed secret")?;
|
relicario_core::imgsecret::embed(&carrier, &image_secret).context("failed to embed secret")?;
|
||||||
|
|
||||||
// 4. Save reference JPEG
|
// 4. Save reference JPEG
|
||||||
fs::write(&output, &reference_jpeg).context("failed to write reference image")?;
|
fs::write(&output, &reference_jpeg).context("failed to write reference image")?;
|
||||||
@@ -370,22 +370,22 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
|||||||
|
|
||||||
// 7. Derive master key
|
// 7. Derive master key
|
||||||
let params = KdfParams::default();
|
let params = KdfParams::default();
|
||||||
let master_key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||||
.context("failed to derive master key")?;
|
.context("failed to derive master key")?;
|
||||||
|
|
||||||
// 8. Create directory structure
|
// 8. Create directory structure
|
||||||
let idfoto = idfoto_dir();
|
let relicario = relicario_dir();
|
||||||
fs::create_dir_all(&idfoto).context("failed to create .idfoto directory")?;
|
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")?;
|
fs::create_dir_all(vault_dir().join("entries")).context("failed to create entries directory")?;
|
||||||
|
|
||||||
// 9. Write config files
|
// 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(
|
fs::write(
|
||||||
idfoto.join("params.json"),
|
relicario.join("params.json"),
|
||||||
serde_json::to_string_pretty(¶ms)?,
|
serde_json::to_string_pretty(¶ms)?,
|
||||||
)
|
)
|
||||||
.context("failed to write params.json")?;
|
.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
|
// 10. Encrypt empty manifest
|
||||||
let manifest = Manifest::new();
|
let manifest = Manifest::new();
|
||||||
@@ -403,7 +403,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
|||||||
if !status.success() {
|
if !status.success() {
|
||||||
bail!("git init failed");
|
bail!("git init failed");
|
||||||
}
|
}
|
||||||
git_commit("feat: initialize idfoto vault")?;
|
git_commit("feat: initialize relicario vault")?;
|
||||||
|
|
||||||
// 13. Success
|
// 13. Success
|
||||||
eprintln!("Vault initialized successfully.");
|
eprintln!("Vault initialized successfully.");
|
||||||
@@ -757,24 +757,24 @@ fn cmd_sync() -> Result<()> {
|
|||||||
|
|
||||||
// ─── Device management ──────────────────────────────────────────────────────
|
// ─── Device management ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Read the device registry from `.idfoto/devices.json`.
|
/// Read the device registry from `.relicario/devices.json`.
|
||||||
fn read_devices() -> Result<Vec<DeviceEntry>> {
|
fn read_devices() -> Result<Vec<DeviceEntry>> {
|
||||||
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 data = fs::read_to_string(&path).context("failed to read devices.json")?;
|
||||||
let devices: Vec<DeviceEntry> = serde_json::from_str(&data).context("failed to parse devices.json")?;
|
let devices: Vec<DeviceEntry> = serde_json::from_str(&data).context("failed to parse devices.json")?;
|
||||||
Ok(devices)
|
Ok(devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the device registry to `.idfoto/devices.json`.
|
/// Write the device registry to `.relicario/devices.json`.
|
||||||
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
||||||
let data = serde_json::to_string_pretty(devices)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a new device by generating an ed25519 keypair.
|
/// Register a new device by generating an ed25519 keypair.
|
||||||
///
|
///
|
||||||
/// The private key is saved to `~/.config/idfoto/<name>.key` with
|
/// The private key is saved to `~/.config/relicario/<name>.key` with
|
||||||
/// restrictive permissions (0600 on Unix). The public key is added to
|
/// restrictive permissions (0600 on Unix). The public key is added to
|
||||||
/// the vault's devices.json and committed to git.
|
/// the vault's devices.json and committed to git.
|
||||||
///
|
///
|
||||||
@@ -800,7 +800,7 @@ fn cmd_device_add(name: String) -> Result<()> {
|
|||||||
// Save private key to the user's config directory (NOT in the vault)
|
// Save private key to the user's config directory (NOT in the vault)
|
||||||
let config_dir = dirs::config_dir()
|
let config_dir = dirs::config_dir()
|
||||||
.context("failed to find config directory")?
|
.context("failed to find config directory")?
|
||||||
.join("idfoto");
|
.join("relicario");
|
||||||
fs::create_dir_all(&config_dir).context("failed to create config directory")?;
|
fs::create_dir_all(&config_dir).context("failed to create config directory")?;
|
||||||
let key_path = config_dir.join(format!("{}.key", name));
|
let key_path = config_dir.join(format!("{}.key", name));
|
||||||
fs::write(&key_path, &private_key_hex).context("failed to write private key")?;
|
fs::write(&key_path, &private_key_hex).context("failed to write private key")?;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "idfoto-core"
|
name = "relicario-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Core library for idfoto password manager"
|
description = "Core library for relicario password manager"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Both factors contribute to the derived key -- compromising one without the
|
//! Both factors contribute to the derived key -- compromising one without the
|
||||||
//! other is insufficient. The salt is vault-specific and stored in `.idfoto/salt`.
|
//! other is insufficient. The salt is vault-specific and stored in `.relicario/salt`.
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2, Params, Version};
|
use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
@@ -51,7 +51,7 @@ use chacha20poly1305::{
|
|||||||
use rand::{rngs::OsRng, RngCore};
|
use rand::{rngs::OsRng, RngCore};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::{IdfotoError, Result};
|
use crate::error::{RelicarioError, Result};
|
||||||
|
|
||||||
/// Current binary format version. Increment this if the ciphertext layout changes.
|
/// Current binary format version. Increment this if the ciphertext layout changes.
|
||||||
const VERSION_BYTE: u8 = 0x01;
|
const VERSION_BYTE: u8 = 0x01;
|
||||||
@@ -74,7 +74,7 @@ const HEADER_LEN: usize = 1 + NONCE_LEN; // version + nonce
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`IdfotoError::Encrypt`] if the underlying AEAD operation fails
|
/// Returns [`RelicarioError::Encrypt`] if the underlying AEAD operation fails
|
||||||
/// (extremely unlikely in practice).
|
/// (extremely unlikely in practice).
|
||||||
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||||
let cipher = XChaCha20Poly1305::new(key.into());
|
let cipher = XChaCha20Poly1305::new(key.into());
|
||||||
@@ -88,7 +88,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
let ciphertext = cipher
|
let ciphertext = cipher
|
||||||
.encrypt(&nonce, plaintext)
|
.encrypt(&nonce, plaintext)
|
||||||
.map_err(|e| IdfotoError::Encrypt(e.to_string()))?;
|
.map_err(|e| RelicarioError::Encrypt(e.to_string()))?;
|
||||||
|
|
||||||
// Output: version(1) || nonce(24) || ciphertext+tag
|
// Output: version(1) || nonce(24) || ciphertext+tag
|
||||||
let mut output = Vec::with_capacity(HEADER_LEN + ciphertext.len());
|
let mut output = Vec::with_capacity(HEADER_LEN + ciphertext.len());
|
||||||
@@ -103,27 +103,27 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
|||||||
///
|
///
|
||||||
/// Validates the version byte and minimum blob length before attempting
|
/// Validates the version byte and minimum blob length before attempting
|
||||||
/// authenticated decryption. If the key is wrong or the data has been
|
/// authenticated decryption. If the key is wrong or the data has been
|
||||||
/// tampered with, the Poly1305 tag verification fails and [`IdfotoError::Decrypt`]
|
/// tampered with, the Poly1305 tag verification fails and [`RelicarioError::Decrypt`]
|
||||||
/// is returned -- with no information about which bytes were wrong (preventing
|
/// is returned -- with no information about which bytes were wrong (preventing
|
||||||
/// padding oracle / chosen-ciphertext attacks).
|
/// padding oracle / chosen-ciphertext attacks).
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`IdfotoError::Format`] if the data is too short or has an unknown version byte.
|
/// - [`RelicarioError::Format`] if the data is too short or has an unknown version byte.
|
||||||
/// - [`IdfotoError::Decrypt`] if the AEAD tag verification fails (wrong key or
|
/// - [`RelicarioError::Decrypt`] if the AEAD tag verification fails (wrong key or
|
||||||
/// tampered data).
|
/// tampered data).
|
||||||
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||||
// Minimum valid blob: 1 (version) + 24 (nonce) + 16 (tag) = 41 bytes.
|
// Minimum valid blob: 1 (version) + 24 (nonce) + 16 (tag) = 41 bytes.
|
||||||
// A zero-length plaintext produces exactly 41 bytes of output.
|
// A zero-length plaintext produces exactly 41 bytes of output.
|
||||||
if data.len() < HEADER_LEN + TAG_LEN {
|
if data.len() < HEADER_LEN + TAG_LEN {
|
||||||
return Err(IdfotoError::Format(
|
return Err(RelicarioError::Format(
|
||||||
"data too short to be valid ciphertext".into(),
|
"data too short to be valid ciphertext".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = data[0];
|
let version = data[0];
|
||||||
if version != VERSION_BYTE {
|
if version != VERSION_BYTE {
|
||||||
return Err(IdfotoError::Format(format!(
|
return Err(RelicarioError::Format(format!(
|
||||||
"unknown version byte: 0x{:02x}",
|
"unknown version byte: 0x{:02x}",
|
||||||
version
|
version
|
||||||
)));
|
)));
|
||||||
@@ -135,14 +135,14 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
|||||||
let cipher = XChaCha20Poly1305::new(key.into());
|
let cipher = XChaCha20Poly1305::new(key.into());
|
||||||
let plaintext = cipher
|
let plaintext = cipher
|
||||||
.decrypt(nonce, ciphertext)
|
.decrypt(nonce, ciphertext)
|
||||||
.map_err(|_| IdfotoError::Decrypt)?;
|
.map_err(|_| RelicarioError::Decrypt)?;
|
||||||
|
|
||||||
Ok(plaintext)
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tunable parameters for the Argon2id key derivation function.
|
/// Tunable parameters for the Argon2id key derivation function.
|
||||||
///
|
///
|
||||||
/// These are stored in the vault's `.idfoto/params.json` so that every client
|
/// These are stored in the vault's `.relicario/params.json` so that every client
|
||||||
/// derives the same master key from the same inputs. Making them configurable
|
/// derives the same master key from the same inputs. Making them configurable
|
||||||
/// lets tests use fast params (m=256, t=1, p=1) while production uses strong
|
/// lets tests use fast params (m=256, t=1, p=1) while production uses strong
|
||||||
/// params (m=64MiB, t=3, p=4).
|
/// params (m=64MiB, t=3, p=4).
|
||||||
@@ -191,8 +191,8 @@ impl Default for KdfParams {
|
|||||||
/// - `passphrase`: the user's passphrase as raw UTF-8 bytes.
|
/// - `passphrase`: the user's passphrase as raw UTF-8 bytes.
|
||||||
/// - `image_secret`: the 32-byte secret extracted from the reference JPEG via
|
/// - `image_secret`: the 32-byte secret extracted from the reference JPEG via
|
||||||
/// [`crate::imgsecret::extract`].
|
/// [`crate::imgsecret::extract`].
|
||||||
/// - `salt`: a 32-byte vault-specific salt (stored in `.idfoto/salt`).
|
/// - `salt`: a 32-byte vault-specific salt (stored in `.relicario/salt`).
|
||||||
/// - `params`: the Argon2id tuning parameters (stored in `.idfoto/params.json`).
|
/// - `params`: the Argon2id tuning parameters (stored in `.relicario/params.json`).
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
@@ -200,7 +200,7 @@ impl Default for KdfParams {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`IdfotoError::Kdf`] if the Argon2id parameters are invalid (e.g.,
|
/// Returns [`RelicarioError::Kdf`] if the Argon2id parameters are invalid (e.g.,
|
||||||
/// memory cost below the library's minimum).
|
/// memory cost below the library's minimum).
|
||||||
pub fn derive_master_key(
|
pub fn derive_master_key(
|
||||||
passphrase: &[u8],
|
passphrase: &[u8],
|
||||||
@@ -214,7 +214,7 @@ pub fn derive_master_key(
|
|||||||
params.argon2_p,
|
params.argon2_p,
|
||||||
Some(32),
|
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 argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ pub fn derive_master_key(
|
|||||||
let mut output = [0u8; 32];
|
let mut output = [0u8; 32];
|
||||||
argon2
|
argon2
|
||||||
.hash_password_into(&password, salt, &mut output)
|
.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)
|
Ok(output)
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn encrypt_decrypt_round_trip() {
|
fn encrypt_decrypt_round_trip() {
|
||||||
let key = [0xABu8; 32];
|
let key = [0xABu8; 32];
|
||||||
let plaintext = b"hello, idfoto!";
|
let plaintext = b"hello, relicario!";
|
||||||
|
|
||||||
let ciphertext = encrypt(&key, plaintext).unwrap();
|
let ciphertext = encrypt(&key, plaintext).unwrap();
|
||||||
let decrypted = decrypt(&key, &ciphertext).unwrap();
|
let decrypted = decrypt(&key, &ciphertext).unwrap();
|
||||||
@@ -307,7 +307,7 @@ mod tests {
|
|||||||
let result = decrypt(&wrong_key, &ciphertext);
|
let result = decrypt(&wrong_key, &ciphertext);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(matches!(result.unwrap_err(), IdfotoError::Decrypt));
|
assert!(matches!(result.unwrap_err(), RelicarioError::Decrypt));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
//! Unified error type for the idfoto-core crate.
|
//! Unified error type for the relicario-core crate.
|
||||||
//!
|
//!
|
||||||
//! Every fallible function in this crate returns [`Result<T>`], which is an alias
|
//! Every fallible function in this crate returns [`Result<T>`], which is an alias
|
||||||
//! for `std::result::Result<T, IdfotoError>`. Using a single error enum keeps the
|
//! for `std::result::Result<T, RelicarioError>`. Using a single error enum keeps the
|
||||||
//! public API surface predictable and makes error handling in callers (CLI, WASM
|
//! public API surface predictable and makes error handling in callers (CLI, WASM
|
||||||
//! bindings, mobile FFI) straightforward.
|
//! bindings, mobile FFI) straightforward.
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// All errors that can originate from idfoto-core operations.
|
/// All errors that can originate from relicario-core operations.
|
||||||
///
|
///
|
||||||
/// Variants are ordered roughly by the pipeline stage where they occur:
|
/// Variants are ordered roughly by the pipeline stage where they occur:
|
||||||
/// KDF -> encryption -> decryption -> format parsing -> entry lookup -> image
|
/// KDF -> encryption -> decryption -> format parsing -> entry lookup -> image
|
||||||
/// steganography -> serialization -> device keys.
|
/// steganography -> serialization -> device keys.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum IdfotoError {
|
pub enum RelicarioError {
|
||||||
/// The Argon2id key derivation failed. This typically means invalid KDF
|
/// The Argon2id key derivation failed. This typically means invalid KDF
|
||||||
/// parameters were supplied (e.g., memory cost below Argon2's minimum).
|
/// parameters were supplied (e.g., memory cost below Argon2's minimum).
|
||||||
#[error("key derivation failed: {0}")]
|
#[error("key derivation failed: {0}")]
|
||||||
@@ -83,4 +83,4 @@ pub enum IdfotoError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Crate-wide result alias, reducing boilerplate in function signatures.
|
/// Crate-wide result alias, reducing boilerplate in function signatures.
|
||||||
pub type Result<T> = std::result::Result<T, IdfotoError>;
|
pub type Result<T> = std::result::Result<T, RelicarioError>;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! DCT-based steganographic embedding of a 256-bit secret in JPEG images.
|
//! 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
|
//! JPEG image's luminance channel using Quantization Index Modulation (QIM) on
|
||||||
//! mid-frequency DCT coefficients, with majority voting across multiple redundant
|
//! mid-frequency DCT coefficients, with majority voting across multiple redundant
|
||||||
//! copies for robustness.
|
//! copies for robustness.
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
//! - Mild cropping (up to ~10% from edges, within the 15% crumple zone)
|
//! - Mild cropping (up to ~10% from edges, within the 15% crumple zone)
|
||||||
//! - Color space conversions (embedding is in luminance only)
|
//! - Color space conversions (embedding is in luminance only)
|
||||||
|
|
||||||
use crate::error::{IdfotoError, Result};
|
use crate::error::{RelicarioError, Result};
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
use image::codecs::jpeg::JpegEncoder;
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use image::{ImageEncoder, Rgb, RgbImage};
|
use image::{ImageEncoder, Rgb, RgbImage};
|
||||||
@@ -179,10 +179,10 @@ struct EmbedRegion {
|
|||||||
fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
||||||
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
||||||
.with_guessed_format()
|
.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
|
let img = reader
|
||||||
.decode()
|
.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 rgb = img.to_rgb8();
|
||||||
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
||||||
let mut data = Vec::with_capacity(width * height);
|
let mut data = Vec::with_capacity(width * height);
|
||||||
@@ -527,10 +527,10 @@ fn select_embed_blocks(region: &EmbedRegion, target_count: usize) -> Vec<(usize,
|
|||||||
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u8>> {
|
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u8>> {
|
||||||
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
||||||
.with_guessed_format()
|
.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
|
let img = reader
|
||||||
.decode()
|
.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 rgb = img.to_rgb8();
|
||||||
let (width, height) = (rgb.width(), rgb.height());
|
let (width, height) = (rgb.width(), rgb.height());
|
||||||
|
|
||||||
@@ -572,7 +572,7 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
|||||||
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
|
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
|
||||||
encoder
|
encoder
|
||||||
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
|
.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)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,14 +597,14 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`IdfotoError::ImageTooSmall`] if the image is below minimum dimensions
|
/// - [`RelicarioError::ImageTooSmall`] if the image is below minimum dimensions
|
||||||
/// or does not have enough blocks for reliable embedding.
|
/// or does not have enough blocks for reliable embedding.
|
||||||
/// - [`IdfotoError::ImgSecret`] if the image cannot be decoded or re-encoded.
|
/// - [`RelicarioError::ImgSecret`] if the image cannot be decoded or re-encoded.
|
||||||
pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||||
let mut y = extract_y_channel(carrier_jpeg)?;
|
let mut y = extract_y_channel(carrier_jpeg)?;
|
||||||
|
|
||||||
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
||||||
return Err(IdfotoError::ImageTooSmall {
|
return Err(RelicarioError::ImageTooSmall {
|
||||||
min_width: MIN_DIMENSION,
|
min_width: MIN_DIMENSION,
|
||||||
min_height: MIN_DIMENSION,
|
min_height: MIN_DIMENSION,
|
||||||
actual_width: y.width as u32,
|
actual_width: y.width as u32,
|
||||||
@@ -616,7 +616,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
|||||||
let total_blocks = region.blocks_x * region.blocks_y;
|
let total_blocks = region.blocks_x * region.blocks_y;
|
||||||
|
|
||||||
if total_blocks < BLOCKS_PER_COPY * MIN_COPIES {
|
if total_blocks < BLOCKS_PER_COPY * MIN_COPIES {
|
||||||
return Err(IdfotoError::ImageTooSmall {
|
return Err(RelicarioError::ImageTooSmall {
|
||||||
min_width: MIN_DIMENSION,
|
min_width: MIN_DIMENSION,
|
||||||
min_height: MIN_DIMENSION,
|
min_height: MIN_DIMENSION,
|
||||||
actual_width: y.width as u32,
|
actual_width: y.width as u32,
|
||||||
@@ -669,7 +669,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`IdfotoError::ExtractionFailed`] if no valid secret could be recovered
|
/// - [`RelicarioError::ExtractionFailed`] if no valid secret could be recovered
|
||||||
/// (image was never watermarked, or was too heavily recompressed/cropped).
|
/// (image was never watermarked, or was too heavily recompressed/cropped).
|
||||||
pub fn extract(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
pub fn extract(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||||
extract_with_crop_recovery(jpeg_bytes)
|
extract_with_crop_recovery(jpeg_bytes)
|
||||||
@@ -695,7 +695,7 @@ fn try_extract_with_layout(
|
|||||||
) -> Result<[u8; 32]> {
|
) -> Result<[u8; 32]> {
|
||||||
let positions = compute_embed_positions(orig_w, orig_h);
|
let positions = compute_embed_positions(orig_w, orig_h);
|
||||||
if positions.is_empty() {
|
if positions.is_empty() {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let region = compute_region(orig_w, orig_h);
|
let region = compute_region(orig_w, orig_h);
|
||||||
@@ -750,14 +750,14 @@ fn try_extract_with_layout(
|
|||||||
let mut result_bits = vec![0u8; SECRET_BITS];
|
let mut result_bits = vec![0u8; SECRET_BITS];
|
||||||
for i in 0..SECRET_BITS {
|
for i in 0..SECRET_BITS {
|
||||||
if votes_total[i] == 0 {
|
if votes_total[i] == 0 {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
let ones = votes_one[i];
|
let ones = votes_one[i];
|
||||||
let zeros = votes_total[i] - ones;
|
let zeros = votes_total[i] - ones;
|
||||||
let majority = ones.max(zeros);
|
let majority = ones.max(zeros);
|
||||||
let confidence = majority as f64 / votes_total[i] as f64;
|
let confidence = majority as f64 / votes_total[i] as f64;
|
||||||
if confidence < 0.60 {
|
if confidence < 0.60 {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
result_bits[i] = if ones > zeros { 1 } else { 0 };
|
result_bits[i] = if ones > zeros { 1 } else { 0 };
|
||||||
}
|
}
|
||||||
@@ -785,7 +785,7 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
|||||||
let y = extract_y_channel(jpeg_bytes)?;
|
let y = extract_y_channel(jpeg_bytes)?;
|
||||||
|
|
||||||
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
if (y.width as u32) < MIN_DIMENSION || (y.height as u32) < MIN_DIMENSION {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try 1: assume the image is uncropped (original size = current size)
|
// Try 1: assume the image is uncropped (original size = current size)
|
||||||
@@ -830,7 +830,7 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(IdfotoError::ExtractionFailed)
|
Err(RelicarioError::ExtractionFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tests ───────────────────────────────────────────────────────────────────
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! # 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 intentionally **bytes-in/bytes-out** -- it performs no filesystem
|
//! 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
|
//! access, no network I/O, and no git operations. All inputs arrive as byte slices
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Modules
|
//! ## Modules
|
||||||
//!
|
//!
|
||||||
//! - [`error`] -- The unified error type ([`IdfotoError`]) used across the crate.
|
//! - [`error`] -- The unified error type ([`RelicarioError`]) used across the crate.
|
||||||
//! - [`crypto`] -- Argon2id key derivation and XChaCha20-Poly1305 authenticated
|
//! - [`crypto`] -- Argon2id key derivation and XChaCha20-Poly1305 authenticated
|
||||||
//! encryption. This is the low-level "encrypt bytes / decrypt bytes" layer.
|
//! encryption. This is the low-level "encrypt bytes / decrypt bytes" layer.
|
||||||
//! - [`entry`] -- The vault data model: [`Entry`] (full credential),
|
//! - [`entry`] -- The vault data model: [`Entry`] (full credential),
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub use error::{IdfotoError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
|
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
||||||
@@ -28,9 +28,9 @@ use crate::error::Result;
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`crate::IdfotoError::Json`] if JSON serialization fails (should not happen
|
/// - [`crate::RelicarioError::Json`] if JSON serialization fails (should not happen
|
||||||
/// with well-formed Entry structs).
|
/// with well-formed Entry structs).
|
||||||
/// - [`crate::IdfotoError::Encrypt`] if the underlying AEAD operation fails.
|
/// - [`crate::RelicarioError::Encrypt`] if the underlying AEAD operation fails.
|
||||||
pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result<Vec<u8>> {
|
pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result<Vec<u8>> {
|
||||||
let json = serde_json::to_vec(entry)?;
|
let json = serde_json::to_vec(entry)?;
|
||||||
crypto::encrypt(master_key, &json)
|
crypto::encrypt(master_key, &json)
|
||||||
@@ -40,10 +40,10 @@ pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result<Vec<u8>> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - [`crate::IdfotoError::Decrypt`] if the master key is wrong or the data is
|
/// - [`crate::RelicarioError::Decrypt`] if the master key is wrong or the data is
|
||||||
/// tampered.
|
/// tampered.
|
||||||
/// - [`crate::IdfotoError::Format`] if the ciphertext blob has an invalid header.
|
/// - [`crate::RelicarioError::Format`] if the ciphertext blob has an invalid header.
|
||||||
/// - [`crate::IdfotoError::Json`] if the decrypted JSON is malformed.
|
/// - [`crate::RelicarioError::Json`] if the decrypted JSON is malformed.
|
||||||
pub fn decrypt_entry(master_key: &[u8; 32], data: &[u8]) -> Result<Entry> {
|
pub fn decrypt_entry(master_key: &[u8; 32], data: &[u8]) -> Result<Entry> {
|
||||||
let json = crypto::decrypt(master_key, data)?;
|
let json = crypto::decrypt(master_key, data)?;
|
||||||
let entry: Entry = serde_json::from_slice(&json)?;
|
let entry: Entry = serde_json::from_slice(&json)?;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use idfoto_core::{
|
use relicario_core::{
|
||||||
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
|
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
|
||||||
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
||||||
};
|
};
|
||||||
@@ -38,10 +38,10 @@ fn full_vault_workflow() {
|
|||||||
// 2. Generate random image_secret and embed
|
// 2. Generate random image_secret and embed
|
||||||
let mut image_secret = [0u8; 32];
|
let mut image_secret = [0u8; 32];
|
||||||
rand::thread_rng().fill_bytes(&mut image_secret);
|
rand::thread_rng().fill_bytes(&mut image_secret);
|
||||||
let stego = idfoto_core::imgsecret::embed(&carrier, &image_secret).unwrap();
|
let stego = relicario_core::imgsecret::embed(&carrier, &image_secret).unwrap();
|
||||||
|
|
||||||
// 3. Extract and verify
|
// 3. Extract and verify
|
||||||
let extracted = idfoto_core::imgsecret::extract(&stego).unwrap();
|
let extracted = relicario_core::imgsecret::extract(&stego).unwrap();
|
||||||
assert_eq!(extracted, image_secret, "extracted image_secret must match embedded");
|
assert_eq!(extracted, image_secret, "extracted image_secret must match embedded");
|
||||||
|
|
||||||
// 4. Derive master_key with fast params
|
// 4. Derive master_key with fast params
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "idfoto-wasm"
|
name = "relicario-wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "WASM bindings for idfoto password manager"
|
description = "WASM bindings for relicario password manager"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idfoto-core = { path = "../idfoto-core" }
|
relicario-core = { path = "../relicario-core" }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! WASM bindings for the idfoto password manager.
|
//! WASM bindings for the relicario password manager.
|
||||||
//!
|
//!
|
||||||
//! This crate wraps [`idfoto_core`] for use in a Chrome MV3 browser extension via
|
//! This crate wraps [`relicario_core`] for use in a Chrome MV3 browser extension via
|
||||||
//! `wasm-bindgen`. Every function marked `#[wasm_bindgen]` is callable from
|
//! `wasm-bindgen`. Every function marked `#[wasm_bindgen]` is callable from
|
||||||
//! JavaScript after loading the compiled `.wasm` module.
|
//! JavaScript after loading the compiled `.wasm` module.
|
||||||
//!
|
//!
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use idfoto_core::crypto::{self, KdfParams};
|
use relicario_core::crypto::{self, KdfParams};
|
||||||
use idfoto_core::entry::Entry;
|
use relicario_core::entry::Entry;
|
||||||
use idfoto_core::vault;
|
use relicario_core::vault;
|
||||||
use idfoto_core::imgsecret;
|
use relicario_core::imgsecret;
|
||||||
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
@@ -103,7 +103,7 @@ pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>,
|
|||||||
let secret: [u8; 32] = secret
|
let secret: [u8; 32] = secret
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
|
.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()))
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsVa
|
|||||||
let key: &[u8; 32] = key
|
let key: &[u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
let manifest: idfoto_core::entry::Manifest =
|
let manifest: relicario_core::entry::Manifest =
|
||||||
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
serde_json::from_str(manifest_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
vault::encrypt_manifest(key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
vault::encrypt_manifest(key, &manifest).map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# idfoto — Architecture
|
# relicario — Architecture
|
||||||
|
|
||||||
## System Overview
|
## System Overview
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
│ CLIENT DEVICE (trusted) │
|
│ CLIENT DEVICE (trusted) │
|
||||||
│ │
|
│ │
|
||||||
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||||
│ │ Reference │ │ Passphrase │ │ idfoto-cli │ │
|
│ │ Reference │ │ Passphrase │ │ relicario-cli │ │
|
||||||
│ │ JPEG │ │ (typed) │ │ or browser ext │ │
|
│ │ JPEG │ │ (typed) │ │ or browser ext │ │
|
||||||
│ │ (on disk) │ │ │ │ │ │
|
│ │ (on disk) │ │ │ │ │ │
|
||||||
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
|
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
│ GIT SERVER (untrusted) │
|
│ GIT SERVER (untrusted) │
|
||||||
│ │
|
│ │
|
||||||
│ idfoto-vault.git/ │
|
│ relicario-vault.git/ │
|
||||||
│ ├── manifest.enc ← opaque ciphertext │
|
│ ├── manifest.enc ← opaque ciphertext │
|
||||||
│ ├── entries/ │
|
│ ├── entries/ │
|
||||||
│ │ ├── a1b2c3d4.enc ← opaque ciphertext │
|
│ │ ├── a1b2c3d4.enc ← opaque ciphertext │
|
||||||
│ │ └── e5f6a7b8.enc ← opaque ciphertext │
|
│ │ └── e5f6a7b8.enc ← opaque ciphertext │
|
||||||
│ └── .idfoto/ │
|
│ └── .relicario/ │
|
||||||
│ ├── salt ← 32 bytes (not secret) │
|
│ ├── salt ← 32 bytes (not secret) │
|
||||||
│ ├── params.json ← KDF params (not secret) │
|
│ ├── params.json ← KDF params (not secret) │
|
||||||
│ └── devices.json ← device public keys (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 │
|
│ 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
|
│ uses
|
||||||
▼
|
▼
|
||||||
┌────────────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────────────┐
|
||||||
│ idfoto-core │
|
│ relicario-core │
|
||||||
│ Platform-agnostic: bytes in, bytes out │
|
│ Platform-agnostic: bytes in, bytes out │
|
||||||
│ No filesystem, no network, no git │
|
│ No filesystem, no network, no git │
|
||||||
│ │
|
│ │
|
||||||
@@ -230,7 +230,7 @@ Input JPEG (possibly re-encoded or cropped)
|
|||||||
│ │ │ │ QIM │ │ │ │ manifest() │ │
|
│ │ │ │ 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 │
|
│ Future: JNI/Swift wrappers for Android/iOS │
|
||||||
└────────────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# idfoto Security Audit Report
|
# relicario Security Audit Report
|
||||||
|
|
||||||
**Date:** 2026-04-18
|
**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.
|
**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:**
|
**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.
|
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.
|
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.
|
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 `<img>` tags is blocked. But:
|
The textContent round-trip *does* escape `<`, `>`, and `&`, so injection of raw `<img>` 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 `<strong>` text to learn the username being saved.
|
- Wait for the prompt to appear via `MutationObserver`, read the `<strong>` 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()` `#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()` `#idfoto-never-btn` to suppress capture for the user's *real* sites by getting them blacklisted via a confusable hostname.
|
- 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.
|
**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.
|
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.
|
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.
|
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.
|
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:**
|
**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
|
### 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.
|
**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<u8>` from `derive_master_key` and intermediate buffers leak into reallocated heap
|
### H2. Master key never zeroized; `Vec<u8>` 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.
|
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:**
|
**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.
|
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<Vec<u8>>` so its contents are wiped on drop.
|
3. Wrap the temporary `password` Vec in `Zeroizing<Vec<u8>>` so its contents are wiped on drop.
|
||||||
4. In the CLI, zeroize the passphrase string immediately after passing into `derive_master_key`.
|
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
|
### 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.
|
**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
|
### 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 <message>`. There are no environmental guards:
|
**Issue:** Every CLI mutation runs `git add -A` then `git commit -m <message>`. There are no environmental guards:
|
||||||
|
|
||||||
@@ -189,13 +189,13 @@ Command::new("git")
|
|||||||
"-c", "core.editor=true", "commit", "-m", message])
|
"-c", "core.editor=true", "commit", "-m", message])
|
||||||
```
|
```
|
||||||
|
|
||||||
Stage only the specific files the operation touched (`entries/<id>.enc`, `manifest.enc`, `.idfoto/devices.json`) instead of `git add -A`.
|
Stage only the specific files the operation touched (`entries/<id>.enc`, `manifest.enc`, `.relicario/devices.json`) instead of `git add -A`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### H5. WASM `generate_password` uses `Math.random()` — claimed "non-security-critical" is wrong
|
### 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:
|
**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/<id>.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.
|
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
|
```rust
|
||||||
use rand::{rngs::OsRng, RngCore};
|
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
|
### 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.
|
**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
|
### 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.
|
**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()`
|
### 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`
|
### 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
|
### 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
|
### 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
|
### 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
|
### 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<String>`.
|
`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<String>`.
|
||||||
|
|
||||||
### M7. CLI prints the full password to stdout via `println!`
|
### M7. CLI prints the full password to stdout via `println!`
|
||||||
|
|
||||||
`crates/idfoto-cli/src/main.rs:553`. `idfoto get` prints `"Password: <plaintext>"` 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: <plaintext>"` 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
|
### 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
|
### 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
|
### 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
|
### 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
|
### 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
|
## LOW / INFORMATIONAL
|
||||||
|
|
||||||
- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/idfoto-core/src/imgsecret.rs:163, 166`).
|
- **L1.** Dead-code-allowed fields in `EmbedRegion` (`crates/relicario-core/src/imgsecret.rs:163, 166`).
|
||||||
- **L2.** `IdfotoError::Format` exposes the offending version byte in user-facing error string. Minor info disclosure.
|
- **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.
|
- **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.
|
- **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.
|
- **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`.
|
- **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.
|
- **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.
|
- **L9.** `devices.json` initial write differs between CLI (`"[]"`) and extension (`'{"devices":[]}'`). Schema mismatch.
|
||||||
- **L10.** `totpSecretCache` (`Map<string, string>` of plaintext base32 secrets) has no zeroization — note that JS strings can't be zeroized.
|
- **L10.** `totpSecretCache` (`Map<string, string>` 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.
|
- **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`).
|
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.
|
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`).
|
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.
|
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`.
|
9. **Two-factor independence** verified by `tests/integration.rs:120-153`.
|
||||||
10. **DCT round-trip correctness** verified to 1e-6 tolerance.
|
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
|
## 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.
|
**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.
|
||||||
|
|||||||
@@ -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.
|
> **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.
|
**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
|
**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.
|
**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
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
idfoto/ (project root = /home/alee/Sources/idfoto)
|
relicario/ (project root = /home/alee/Sources/relicario)
|
||||||
├── Cargo.toml # workspace root
|
├── Cargo.toml # workspace root
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── idfoto-core/
|
│ ├── relicario-core/
|
||||||
│ │ ├── Cargo.toml
|
│ │ ├── Cargo.toml
|
||||||
│ │ └── src/
|
│ │ └── src/
|
||||||
│ │ ├── lib.rs # re-exports public API
|
│ │ ├── lib.rs # re-exports public API
|
||||||
│ │ ├── error.rs # IdfotoError enum (thiserror)
|
│ │ ├── error.rs # RelicarioError enum (thiserror)
|
||||||
│ │ ├── crypto.rs # derive_master_key(), encrypt(), decrypt()
|
│ │ ├── crypto.rs # derive_master_key(), encrypt(), decrypt()
|
||||||
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs
|
│ │ ├── entry.rs # Entry, ManifestEntry, Manifest structs
|
||||||
│ │ ├── vault.rs # encrypt/decrypt entries + manifest, binary format
|
│ │ ├── vault.rs # encrypt/decrypt entries + manifest, binary format
|
||||||
│ │ └── imgsecret.rs # embed(), extract() — DCT embedding primitive
|
│ │ └── imgsecret.rs # embed(), extract() — DCT embedding primitive
|
||||||
│ └── idfoto-cli/
|
│ └── relicario-cli/
|
||||||
│ ├── Cargo.toml
|
│ ├── Cargo.toml
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ └── main.rs # clap CLI with all subcommands
|
│ └── main.rs # clap CLI with all subcommands
|
||||||
├── docs/
|
├── docs/
|
||||||
│ └── superpowers/
|
│ └── superpowers/
|
||||||
│ ├── specs/
|
│ ├── specs/
|
||||||
│ │ └── 2026-04-11-idfoto-design.md
|
│ │ └── 2026-04-11-relicario-design.md
|
||||||
│ └── plans/
|
│ └── plans/
|
||||||
│ └── 2026-04-11-idfoto-core-cli.md (this file)
|
│ └── 2026-04-11-relicario-core-cli.md (this file)
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -50,10 +50,10 @@ idfoto/ (project root = /home/alee/Sources/idfoto)
|
|||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `Cargo.toml`
|
- Create: `Cargo.toml`
|
||||||
- Create: `crates/idfoto-core/Cargo.toml`
|
- Create: `crates/relicario-core/Cargo.toml`
|
||||||
- Create: `crates/idfoto-core/src/lib.rs`
|
- Create: `crates/relicario-core/src/lib.rs`
|
||||||
- Create: `crates/idfoto-cli/Cargo.toml`
|
- Create: `crates/relicario-cli/Cargo.toml`
|
||||||
- Create: `crates/idfoto-cli/src/main.rs`
|
- Create: `crates/relicario-cli/src/main.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Create workspace root Cargo.toml**
|
- [ ] **Step 1: Create workspace root Cargo.toml**
|
||||||
|
|
||||||
@@ -62,20 +62,20 @@ idfoto/ (project root = /home/alee/Sources/idfoto)
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/idfoto-core",
|
"crates/relicario-core",
|
||||||
"crates/idfoto-cli",
|
"crates/relicario-cli",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Create idfoto-core crate**
|
- [ ] **Step 2: Create relicario-core crate**
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# crates/idfoto-core/Cargo.toml
|
# crates/relicario-core/Cargo.toml
|
||||||
[package]
|
[package]
|
||||||
name = "idfoto-core"
|
name = "relicario-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Core library for idfoto password manager"
|
description = "Core library for relicario password manager"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -92,26 +92,26 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
|||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod error;
|
pub mod error;
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Create idfoto-cli crate**
|
- [ ] **Step 3: Create relicario-cli crate**
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# crates/idfoto-cli/Cargo.toml
|
# crates/relicario-cli/Cargo.toml
|
||||||
[package]
|
[package]
|
||||||
name = "idfoto-cli"
|
name = "relicario-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "CLI for idfoto password manager"
|
description = "CLI for relicario password manager"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "idfoto"
|
name = "relicario"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idfoto-core = { path = "../idfoto-core" }
|
relicario-core = { path = "../relicario-core" }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
rpassword = "5"
|
rpassword = "5"
|
||||||
@@ -120,9 +120,9 @@ dirs = "5"
|
|||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-cli/src/main.rs
|
// crates/relicario-cli/src/main.rs
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("idfoto v0.1.0");
|
println!("relicario v0.1.0");
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ git init
|
|||||||
echo "target/" > .gitignore
|
echo "target/" > .gitignore
|
||||||
echo ".superpowers/" >> .gitignore
|
echo ".superpowers/" >> .gitignore
|
||||||
git add Cargo.toml crates/ .gitignore docs/
|
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
|
### Task 2: Error Types
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-core/src/error.rs`
|
- Create: `crates/relicario-core/src/error.rs`
|
||||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
- Modify: `crates/relicario-core/src/lib.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write the error enum**
|
- [ ] **Step 1: Write the error enum**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/error.rs
|
// crates/relicario-core/src/error.rs
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum IdfotoError {
|
pub enum RelicarioError {
|
||||||
#[error("key derivation failed: {0}")]
|
#[error("key derivation failed: {0}")]
|
||||||
Kdf(String),
|
Kdf(String),
|
||||||
|
|
||||||
@@ -193,16 +193,16 @@ pub enum IdfotoError {
|
|||||||
DeviceKey(String),
|
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**
|
- [ ] **Step 2: Update lib.rs to re-export**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
pub use error::{IdfotoError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Verify build**
|
- [ ] **Step 3: Verify build**
|
||||||
@@ -213,8 +213,8 @@ Expected: Compiles cleanly.
|
|||||||
- [ ] **Step 4: Commit**
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/idfoto-core/src/error.rs crates/idfoto-core/src/lib.rs
|
git add crates/relicario-core/src/error.rs crates/relicario-core/src/lib.rs
|
||||||
git commit -m "feat: add IdfotoError enum with thiserror"
|
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
|
### Task 3: Crypto — Key Derivation
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-core/src/crypto.rs`
|
- Create: `crates/relicario-core/src/crypto.rs`
|
||||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
- Modify: `crates/relicario-core/src/lib.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing test**
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/crypto.rs
|
// crates/relicario-core/src/crypto.rs
|
||||||
|
|
||||||
// ... (implementation comes in step 3)
|
// ... (implementation comes in step 3)
|
||||||
|
|
||||||
@@ -274,17 +274,17 @@ mod tests {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run test to verify it fails**
|
- [ ] **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.
|
Expected: FAIL — `derive_master_key` and `KdfParams` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write the implementation**
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/crypto.rs
|
// crates/relicario-core/src/crypto.rs
|
||||||
use argon2::{Algorithm, Argon2, Params, Version};
|
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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct KdfParams {
|
pub struct KdfParams {
|
||||||
/// Memory cost in KiB (default: 65536 = 64 MiB)
|
/// 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.
|
/// Derive a 32-byte master key from passphrase + image_secret + salt.
|
||||||
///
|
///
|
||||||
/// password = passphrase_bytes || image_secret_bytes (concatenated)
|
/// 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(
|
pub fn derive_master_key(
|
||||||
passphrase: &[u8],
|
passphrase: &[u8],
|
||||||
image_secret: &[u8; 32],
|
image_secret: &[u8; 32],
|
||||||
@@ -326,14 +326,14 @@ pub fn derive_master_key(
|
|||||||
params.argon2_p,
|
params.argon2_p,
|
||||||
Some(32),
|
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 argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||||
|
|
||||||
let mut output = [0u8; 32];
|
let mut output = [0u8; 32];
|
||||||
argon2
|
argon2
|
||||||
.hash_password_into(&password, salt, &mut output)
|
.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)
|
Ok(output)
|
||||||
}
|
}
|
||||||
@@ -389,24 +389,24 @@ mod tests {
|
|||||||
|
|
||||||
- [ ] **Step 4: Run 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.
|
Expected: All 3 tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 5: Update lib.rs**
|
- [ ] **Step 5: Update lib.rs**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
pub use crypto::{derive_master_key, KdfParams};
|
pub use crypto::{derive_master_key, KdfParams};
|
||||||
pub use error::{IdfotoError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/idfoto-core/src/
|
git add crates/relicario-core/src/
|
||||||
git commit -m "feat: add Argon2id key derivation with tests"
|
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
|
### Task 4: Crypto — Encrypt / Decrypt
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-core/src/crypto.rs`
|
- Modify: `crates/relicario-core/src/crypto.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing tests**
|
- [ ] **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
|
```rust
|
||||||
#[test]
|
#[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**
|
- [ ] **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.
|
Expected: FAIL — `encrypt` and `decrypt` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write the implementation**
|
- [ ] **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
|
```rust
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
@@ -503,7 +503,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
let ciphertext = cipher
|
let ciphertext = cipher
|
||||||
.encrypt(nonce, plaintext)
|
.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());
|
let mut output = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len());
|
||||||
output.push(FORMAT_VERSION);
|
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>> {
|
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||||
let min_len = 1 + NONCE_SIZE + 16; // version + nonce + tag (empty plaintext)
|
let min_len = 1 + NONCE_SIZE + 16; // version + nonce + tag (empty plaintext)
|
||||||
if data.len() < min_len {
|
if data.len() < min_len {
|
||||||
return Err(IdfotoError::Format(format!(
|
return Err(RelicarioError::Format(format!(
|
||||||
"ciphertext too short: {} bytes, need at least {}",
|
"ciphertext too short: {} bytes, need at least {}",
|
||||||
data.len(),
|
data.len(),
|
||||||
min_len
|
min_len
|
||||||
@@ -527,7 +527,7 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
let version = data[0];
|
let version = data[0];
|
||||||
if version != FORMAT_VERSION {
|
if version != FORMAT_VERSION {
|
||||||
return Err(IdfotoError::Format(format!(
|
return Err(RelicarioError::Format(format!(
|
||||||
"unsupported format version: {version}"
|
"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());
|
let cipher = XChaCha20Poly1305::new(key.into());
|
||||||
cipher
|
cipher
|
||||||
.decrypt(nonce, ciphertext)
|
.decrypt(nonce, ciphertext)
|
||||||
.map_err(|_| IdfotoError::Decrypt)
|
.map_err(|_| RelicarioError::Decrypt)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -557,24 +557,24 @@ use rand::RngCore;
|
|||||||
|
|
||||||
- [ ] **Step 5: Run all crypto tests**
|
- [ ] **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).
|
Expected: All tests PASS (3 KDF tests + 4 encrypt/decrypt tests).
|
||||||
|
|
||||||
- [ ] **Step 6: Update lib.rs exports**
|
- [ ] **Step 6: Update lib.rs exports**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
pub use crypto::{derive_master_key, encrypt, decrypt, KdfParams};
|
pub use crypto::{derive_master_key, encrypt, decrypt, KdfParams};
|
||||||
pub use error::{IdfotoError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 5: Entry & Manifest Data Model
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-core/src/entry.rs`
|
- Create: `crates/relicario-core/src/entry.rs`
|
||||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
- Modify: `crates/relicario-core/src/lib.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write tests for serialization**
|
- [ ] **Step 1: Write tests for serialization**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/entry.rs
|
// crates/relicario-core/src/entry.rs
|
||||||
|
|
||||||
// ... (implementation in step 3)
|
// ... (implementation in step 3)
|
||||||
|
|
||||||
@@ -663,13 +663,13 @@ mod tests {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
- [ ] **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.
|
Expected: FAIL — types not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write the implementation**
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/entry.rs
|
// crates/relicario-core/src/entry.rs
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -850,25 +850,25 @@ mod tests {
|
|||||||
- [ ] **Step 4: Update lib.rs**
|
- [ ] **Step 4: Update lib.rs**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
||||||
pub use error::{IdfotoError, Result};
|
pub use error::{RelicarioError, Result};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Run tests**
|
- [ ] **Step 5: Run tests**
|
||||||
|
|
||||||
Run: `cargo test -p idfoto-core entry`
|
Run: `cargo test -p relicario-core entry`
|
||||||
Expected: All 5 entry tests PASS.
|
Expected: All 5 entry tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 6: Vault Operations
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-core/src/vault.rs`
|
- Create: `crates/relicario-core/src/vault.rs`
|
||||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
- Modify: `crates/relicario-core/src/lib.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing tests**
|
- [ ] **Step 1: Write failing tests**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/vault.rs
|
// crates/relicario-core/src/vault.rs
|
||||||
|
|
||||||
// ... (implementation in step 3)
|
// ... (implementation in step 3)
|
||||||
|
|
||||||
@@ -955,13 +955,13 @@ mod tests {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
- [ ] **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.
|
Expected: FAIL — functions not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write the implementation**
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/vault.rs
|
// crates/relicario-core/src/vault.rs
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::entry::{Entry, Manifest};
|
use crate::entry::{Entry, Manifest};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
@@ -1061,7 +1061,7 @@ mod tests {
|
|||||||
- [ ] **Step 4: Update lib.rs**
|
- [ ] **Step 4: Update lib.rs**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -1069,19 +1069,19 @@ pub mod vault;
|
|||||||
|
|
||||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
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};
|
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Run all tests**
|
- [ ] **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).
|
Expected: All tests PASS (KDF + encrypt/decrypt + entry + vault).
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 7: imgsecret — JPEG Decode, Y Channel, Block DCT
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-core/src/imgsecret.rs`
|
- Create: `crates/relicario-core/src/imgsecret.rs`
|
||||||
- Modify: `crates/idfoto-core/src/lib.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.
|
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**
|
- [ ] **Step 1: Write tests for DCT round-trip and Y channel extraction**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/imgsecret.rs
|
// crates/relicario-core/src/imgsecret.rs
|
||||||
|
|
||||||
// ... (implementation in step 3)
|
// ... (implementation in step 3)
|
||||||
|
|
||||||
@@ -1179,14 +1179,14 @@ mod tests {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
- [ ] **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.
|
Expected: FAIL — functions not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write the implementation**
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/imgsecret.rs
|
// crates/relicario-core/src/imgsecret.rs
|
||||||
use crate::error::{IdfotoError, Result};
|
use crate::error::{RelicarioError, Result};
|
||||||
use image::io::Reader as ImageReader;
|
use image::io::Reader as ImageReader;
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -1214,11 +1214,11 @@ pub struct EmbedRegion {
|
|||||||
pub fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
pub fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
||||||
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
||||||
.with_guessed_format()
|
.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
|
let img = reader
|
||||||
.decode()
|
.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 rgb = img.to_rgb8();
|
||||||
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
||||||
@@ -1464,7 +1464,7 @@ mod tests {
|
|||||||
- [ ] **Step 4: Update lib.rs**
|
- [ ] **Step 4: Update lib.rs**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/src/lib.rs
|
// crates/relicario-core/src/lib.rs
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -1473,19 +1473,19 @@ pub mod vault;
|
|||||||
|
|
||||||
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
pub use crypto::{derive_master_key, decrypt, encrypt, KdfParams};
|
||||||
pub use entry::{generate_entry_id, Entry, Manifest, ManifestEntry};
|
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};
|
pub use vault::{decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest};
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Run tests**
|
- [ ] **Step 5: Run tests**
|
||||||
|
|
||||||
Run: `cargo test -p idfoto-core imgsecret`
|
Run: `cargo test -p relicario-core imgsecret`
|
||||||
Expected: All 4 tests PASS.
|
Expected: All 4 tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 8: imgsecret — QIM Embedding + Block Selection
|
||||||
|
|
||||||
**Files:**
|
**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.
|
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**
|
- [ ] **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.
|
Expected: FAIL — `qim_embed`, `qim_extract`, `select_embed_blocks`, `QUANT_STEP` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write QIM and block selection implementation**
|
- [ ] **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**
|
- [ ] **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).
|
Expected: All tests PASS (previous 4 + 3 new QIM/block-selection tests).
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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()
|
### Task 9: imgsecret — Full embed() and extract()
|
||||||
|
|
||||||
**Files:**
|
**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.
|
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**
|
- [ ] **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.
|
Expected: FAIL — `embed` and `extract` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write embed() implementation**
|
- [ ] **Step 3: Write embed() implementation**
|
||||||
@@ -1727,7 +1727,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
// Check minimum size
|
// Check minimum size
|
||||||
if y.width < MIN_DIMENSION as usize || y.height < MIN_DIMENSION as usize {
|
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_width: MIN_DIMENSION,
|
||||||
min_height: MIN_DIMENSION,
|
min_height: MIN_DIMENSION,
|
||||||
actual_width: y.width as u32,
|
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
|
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50); // cap at 50 copies
|
||||||
|
|
||||||
if num_copies < MIN_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}"
|
"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_x = region.x_offset as isize + dx;
|
||||||
let new_y = region.y_offset as isize + dy;
|
let new_y = region.y_offset as isize + dy;
|
||||||
if new_x < 0 || new_y < 0 {
|
if new_x < 0 || new_y < 0 {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
region.x_offset = new_x as usize;
|
region.x_offset = new_x as usize;
|
||||||
region.y_offset = new_y 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);
|
let num_copies = (total_blocks / BLOCKS_PER_COPY).min(50);
|
||||||
|
|
||||||
if num_copies < 1 {
|
if num_copies < 1 {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let blocks_needed = num_copies * BLOCKS_PER_COPY;
|
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 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
|
let min_confidence = total_votes * 3 / 4; // at least 75% of votes should agree
|
||||||
if confidence < min_confidence {
|
if confidence < min_confidence {
|
||||||
return Err(IdfotoError::ExtractionFailed);
|
return Err(RelicarioError::ExtractionFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(bits_to_bytes(&secret_bits))
|
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>> {
|
fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u8>> {
|
||||||
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
||||||
.with_guessed_format()
|
.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
|
let img = reader
|
||||||
.decode()
|
.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 rgb = img.to_rgb8();
|
||||||
let (width, height) = (rgb.width(), rgb.height());
|
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);
|
let encoder = JpegEncoder::new_with_quality(&mut buf, 92);
|
||||||
encoder
|
encoder
|
||||||
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
|
.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)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Run tests**
|
- [ ] **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.
|
Expected: All tests PASS including embed/extract round-trip.
|
||||||
|
|
||||||
- [ ] **Step 5: Add a JPEG recompression survival test**
|
- [ ] **Step 5: Add a JPEG recompression survival test**
|
||||||
@@ -1976,13 +1976,13 @@ Add to `mod tests`:
|
|||||||
|
|
||||||
- [ ] **Step 6: Run all tests**
|
- [ ] **Step 6: Run all tests**
|
||||||
|
|
||||||
Run: `cargo test -p idfoto-core`
|
Run: `cargo test -p relicario-core`
|
||||||
Expected: All tests PASS.
|
Expected: All tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 10: imgsecret — Crop Recovery
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write failing crop test**
|
- [ ] **Step 1: Write failing crop test**
|
||||||
|
|
||||||
@@ -2033,7 +2033,7 @@ Add to `mod tests`:
|
|||||||
|
|
||||||
- [ ] **Step 2: Run test to verify it fails**
|
- [ ] **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.
|
Expected: FAIL — `extract_with_crop_recovery` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Write crop recovery implementation**
|
- [ ] **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**
|
- [ ] **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.
|
Expected: All tests PASS including crop recovery.
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 11: CLI — Scaffolding, init, generate
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-cli/src/main.rs`
|
- Modify: `crates/relicario-cli/src/main.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write the clap CLI structure**
|
- [ ] **Step 1: Write the clap CLI structure**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-cli/src/main.rs
|
// crates/relicario-cli/src/main.rs
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use idfoto_core::{
|
use relicario_core::{
|
||||||
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
|
decrypt_entry, decrypt_manifest, derive_master_key, encrypt_entry, encrypt_manifest,
|
||||||
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
||||||
};
|
};
|
||||||
@@ -2148,7 +2148,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[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 {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
@@ -2230,21 +2230,21 @@ fn vault_dir() -> PathBuf {
|
|||||||
PathBuf::from(".")
|
PathBuf::from(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn idfoto_dir() -> PathBuf {
|
fn relicario_dir() -> PathBuf {
|
||||||
vault_dir().join(".idfoto")
|
vault_dir().join(".relicario")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_salt() -> Result<[u8; 32]> {
|
fn read_salt() -> Result<[u8; 32]> {
|
||||||
let bytes = fs::read(idfoto_dir().join("salt"))
|
let bytes = fs::read(relicario_dir().join("salt"))
|
||||||
.context("failed to read .idfoto/salt — is this a vault directory?")?;
|
.context("failed to read .relicario/salt — is this a vault directory?")?;
|
||||||
let mut salt = [0u8; 32];
|
let mut salt = [0u8; 32];
|
||||||
salt.copy_from_slice(&bytes);
|
salt.copy_from_slice(&bytes);
|
||||||
Ok(salt)
|
Ok(salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_params() -> Result<KdfParams> {
|
fn read_params() -> Result<KdfParams> {
|
||||||
let json = fs::read_to_string(idfoto_dir().join("params.json"))
|
let json = fs::read_to_string(relicario_dir().join("params.json"))
|
||||||
.context("failed to read .idfoto/params.json")?;
|
.context("failed to read .relicario/params.json")?;
|
||||||
Ok(serde_json::from_str(&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)
|
let jpeg_bytes = fs::read(image_path)
|
||||||
.context("failed to read reference image")?;
|
.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}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to extract image secret: {e}"))?;
|
||||||
|
|
||||||
let salt = read_salt()?;
|
let salt = read_salt()?;
|
||||||
@@ -2268,9 +2268,9 @@ fn unlock(image_path: &Path) -> Result<[u8; 32]> {
|
|||||||
Ok(master_key)
|
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> {
|
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));
|
return Ok(PathBuf::from(path));
|
||||||
}
|
}
|
||||||
eprint!("Reference image 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);
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut image_secret);
|
||||||
|
|
||||||
println!("Embedding secret into reference image...");
|
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}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to embed secret: {e}"))?;
|
||||||
fs::write(output_path, &stego_jpeg)
|
fs::write(output_path, &stego_jpeg)
|
||||||
.context("failed to write reference image")?;
|
.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}"))?;
|
.map_err(|e| anyhow::anyhow!("key derivation failed: {e}"))?;
|
||||||
|
|
||||||
// 5. Write vault structure
|
// 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::create_dir_all(vault_dir().join("entries"))?;
|
||||||
fs::write(idfoto_dir().join("salt"), salt)?;
|
fs::write(relicario_dir().join("salt"), salt)?;
|
||||||
fs::write(
|
fs::write(
|
||||||
idfoto_dir().join("params.json"),
|
relicario_dir().join("params.json"),
|
||||||
serde_json::to_string_pretty(¶ms)?,
|
serde_json::to_string_pretty(¶ms)?,
|
||||||
)?;
|
)?;
|
||||||
fs::write(idfoto_dir().join("devices.json"), "[]")?;
|
fs::write(relicario_dir().join("devices.json"), "[]")?;
|
||||||
|
|
||||||
// 6. Write empty manifest
|
// 6. Write empty manifest
|
||||||
let manifest = Manifest::new();
|
let manifest = Manifest::new();
|
||||||
@@ -2373,7 +2373,7 @@ fn cmd_init(image_path: &Path, output_path: &Path) -> Result<()> {
|
|||||||
// Add .gitignore
|
// Add .gitignore
|
||||||
fs::write(vault_dir().join(".gitignore"), "reference.jpg\n")?;
|
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!("\nVault initialized successfully!");
|
||||||
println!("IMPORTANT: Keep your reference image ({}) safe — you need it to unlock the vault.", output_path.display());
|
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**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 12: CLI — Device Management
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-cli/src/main.rs`
|
- Modify: `crates/relicario-cli/src/main.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Add device subcommands to the CLI**
|
- [ ] **Step 1: Add device subcommands to the CLI**
|
||||||
|
|
||||||
@@ -2733,14 +2733,14 @@ struct DeviceEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_devices() -> Result<Vec<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")?;
|
.context("failed to read devices.json")?;
|
||||||
Ok(serde_json::from_str(&json)?)
|
Ok(serde_json::from_str(&json)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
||||||
let json = serde_json::to_string_pretty(devices)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2759,7 +2759,7 @@ fn cmd_device_add(name: &str) -> Result<()> {
|
|||||||
// Save private key to local config
|
// Save private key to local config
|
||||||
let config_dir = dirs::config_dir()
|
let config_dir = dirs::config_dir()
|
||||||
.context("no config directory")?
|
.context("no config directory")?
|
||||||
.join("idfoto");
|
.join("relicario");
|
||||||
fs::create_dir_all(&config_dir)?;
|
fs::create_dir_all(&config_dir)?;
|
||||||
fs::write(
|
fs::write(
|
||||||
config_dir.join(format!("{name}.key")),
|
config_dir.join(format!("{name}.key")),
|
||||||
@@ -2806,7 +2806,7 @@ fn cmd_device_revoke(name: &str) -> Result<()> {
|
|||||||
|
|
||||||
- [ ] **Step 3: Add hex dependency**
|
- [ ] **Step 3: Add hex dependency**
|
||||||
|
|
||||||
Add to `crates/idfoto-cli/Cargo.toml` under `[dependencies]`:
|
Add to `crates/relicario-cli/Cargo.toml` under `[dependencies]`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -2824,7 +2824,7 @@ Expected: Compiles cleanly.
|
|||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
### Task 13: Integration Test — Full Vault Workflow
|
||||||
|
|
||||||
**Files:**
|
**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.
|
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**
|
- [ ] **Step 1: Write the integration test**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/idfoto-core/tests/integration.rs
|
// crates/relicario-core/tests/integration.rs
|
||||||
use idfoto_core::*;
|
use relicario_core::*;
|
||||||
use idfoto_core::imgsecret;
|
use relicario_core::imgsecret;
|
||||||
|
|
||||||
fn make_test_jpeg(width: u32, height: u32) -> Vec<u8> {
|
fn make_test_jpeg(width: u32, height: u32) -> Vec<u8> {
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
use image::codecs::jpeg::JpegEncoder;
|
||||||
@@ -2967,7 +2967,7 @@ fn two_factor_independence() {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run integration tests**
|
- [ ] **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.
|
Expected: Both tests PASS.
|
||||||
|
|
||||||
- [ ] **Step 3: Run the full test suite**
|
- [ ] **Step 3: Run the full test suite**
|
||||||
@@ -2978,7 +2978,7 @@ Expected: ALL tests across all crates PASS.
|
|||||||
- [ ] **Step 4: Commit**
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
## Plan 2 Preview
|
||||||
|
|
||||||
After this plan is complete and passing, Plan 2 covers:
|
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
|
- **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
|
- **Extension UX**: passphrase prompt, entry list/search, autofill detection
|
||||||
|
|
||||||
@@ -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.
|
> **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
|
**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
|
### 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/shared/messages.ts # Add new message types
|
||||||
extension/src/service-worker/index.ts # Handle new messages
|
extension/src/service-worker/index.ts # Handle new messages
|
||||||
extension/src/content/detector.ts # Import and init capture
|
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/types.ts`
|
||||||
- Modify: `extension/src/shared/messages.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`:
|
Add at the end of `extension/src/shared/types.ts`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export interface IdfotoSettings {
|
export interface RelicarioSettings {
|
||||||
captureEnabled: boolean;
|
captureEnabled: boolean;
|
||||||
captureStyle: 'bar' | 'toast';
|
captureStyle: 'bar' | 'toast';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: IdfotoSettings = {
|
export const DEFAULT_SETTINGS: RelicarioSettings = {
|
||||||
captureEnabled: false,
|
captureEnabled: false,
|
||||||
captureStyle: 'bar',
|
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: 'check_credential'; url: string; username: string; password: string }
|
||||||
| { type: 'blacklist_site'; hostname: string }
|
| { type: 'blacklist_site'; hostname: string }
|
||||||
| { type: 'get_settings' }
|
| { type: 'get_settings' }
|
||||||
| { type: 'update_settings'; settings: Partial<import('./types').IdfotoSettings> }
|
| { type: 'update_settings'; settings: Partial<import('./types').RelicarioSettings> }
|
||||||
| { type: 'get_blacklist' }
|
| { type: 'get_blacklist' }
|
||||||
| { type: 'remove_blacklist'; hostname: string }
|
| { 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:
|
Add these helper functions to `extension/src/service-worker/index.ts`, after the existing storage helpers:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { IdfotoSettings } from '../shared/types';
|
import type { RelicarioSettings } from '../shared/types';
|
||||||
import { DEFAULT_SETTINGS } 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']);
|
const data = await chrome.storage.local.get(['settings']);
|
||||||
if (!data.settings) return { ...DEFAULT_SETTINGS };
|
if (!data.settings) return { ...DEFAULT_SETTINGS };
|
||||||
return { ...DEFAULT_SETTINGS, ...data.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 });
|
await chrome.storage.local.set({ settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,9 +356,9 @@ export function hookForms(): void {
|
|||||||
|
|
||||||
// --- Prompt UI ---
|
// --- Prompt UI ---
|
||||||
|
|
||||||
/// Remove any existing idfoto prompt from the page.
|
/// Remove any existing relicario prompt from the page.
|
||||||
function removePrompt(): void {
|
function removePrompt(): void {
|
||||||
document.getElementById('idfoto-capture-prompt')?.remove();
|
document.getElementById('relicario-capture-prompt')?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a save/update prompt.
|
/// Show a save/update prompt.
|
||||||
@@ -385,7 +385,7 @@ function showPrompt(
|
|||||||
: `Save login for ${hostname}?`;
|
: `Save login for ${hostname}?`;
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.id = 'idfoto-capture-prompt';
|
container.id = 'relicario-capture-prompt';
|
||||||
|
|
||||||
// Common styles
|
// Common styles
|
||||||
const baseStyles = `
|
const baseStyles = `
|
||||||
@@ -451,7 +451,7 @@ function showPrompt(
|
|||||||
|
|
||||||
// Brand label
|
// Brand label
|
||||||
const brand = document.createElement('span');
|
const brand = document.createElement('span');
|
||||||
brand.textContent = 'idfoto';
|
brand.textContent = 'relicario';
|
||||||
brand.style.cssText = 'color: #58a6ff; font-weight: normal; letter-spacing: 1px;';
|
brand.style.cssText = 'color: #58a6ff; font-weight: normal; letter-spacing: 1px;';
|
||||||
|
|
||||||
// Message text
|
// Message text
|
||||||
@@ -627,7 +627,7 @@ Create `extension/src/popup/components/settings.ts`:
|
|||||||
/// Settings view — configure credential capture and manage blacklist.
|
/// Settings view — configure credential capture and manage blacklist.
|
||||||
|
|
||||||
import { setState, sendMessage, navigate, escapeHtml } from '../popup';
|
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> {
|
export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||||
// Load current settings and blacklist in parallel.
|
// Load current settings and blacklist in parallel.
|
||||||
@@ -636,8 +636,8 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
|||||||
sendMessage({ type: 'get_blacklist' }),
|
sendMessage({ type: 'get_blacklist' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const settings: IdfotoSettings = settingsResp.ok
|
const settings: RelicarioSettings = settingsResp.ok
|
||||||
? settingsResp.data as IdfotoSettings
|
? settingsResp.data as RelicarioSettings
|
||||||
: { captureEnabled: false, captureStyle: 'bar' };
|
: { captureEnabled: false, captureStyle: 'bar' };
|
||||||
|
|
||||||
const blacklist: string[] = blacklistResp.ok
|
const blacklist: string[] = blacklistResp.ok
|
||||||
@@ -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.
|
> **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
|
**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
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "idfoto@adlee.work",
|
"id": "relicario@adlee.work",
|
||||||
"strict_min_version": "128.0"
|
"strict_min_version": "128.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -84,8 +84,8 @@ Create `extension/manifest.firefox.json`:
|
|||||||
"setup.html",
|
"setup.html",
|
||||||
"setup.js",
|
"setup.js",
|
||||||
"styles.css",
|
"styles.css",
|
||||||
"idfoto_wasm_bg.wasm",
|
"relicario_wasm_bg.wasm",
|
||||||
"idfoto_wasm.js"
|
"relicario_wasm.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -126,8 +126,8 @@ module.exports = {
|
|||||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||||
{ from: 'setup.html', to: '.' },
|
{ from: 'setup.html', to: '.' },
|
||||||
{ from: 'icons', to: 'icons' },
|
{ from: 'icons', to: 'icons' },
|
||||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||||
{ from: 'wasm/idfoto_wasm.js', 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",
|
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"dev:firefox": "webpack --config webpack.firefox.config.js --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.
|
// (Chrome) and the default export (Firefox) are available.
|
||||||
|
|
||||||
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
// @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
|
// @ts-ignore TS2307
|
||||||
import * as wasmBindings from '../../wasm/idfoto_wasm.js';
|
import * as wasmBindings from '../../wasm/relicario_wasm.js';
|
||||||
|
|
||||||
type WasmModule = typeof wasmBindings;
|
type WasmModule = typeof wasmBindings;
|
||||||
let wasm: WasmModule | null = null;
|
let wasm: WasmModule | null = null;
|
||||||
@@ -204,12 +204,12 @@ async function initWasm(): Promise<WasmModule> {
|
|||||||
|
|
||||||
if (isServiceWorker) {
|
if (isServiceWorker) {
|
||||||
// Chrome: fetch WASM binary and instantiate synchronously
|
// 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();
|
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||||
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
||||||
} else {
|
} else {
|
||||||
// Firefox: background script — dynamic init works
|
// 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);
|
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:
|
Change the doc comment at the top of the file (line 1) from:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/// Service worker entry point for the idfoto Chrome extension.
|
/// Service worker entry point for the relicario Chrome extension.
|
||||||
```
|
```
|
||||||
|
|
||||||
To:
|
To:
|
||||||
|
|
||||||
```typescript
|
```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
|
/// In Chrome this runs as a service worker (MV3). In Firefox this runs
|
||||||
/// as a persistent background script. WASM loading adapts automatically.
|
/// as a persistent background script. WASM loading adapts automatically.
|
||||||
@@ -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.
|
> **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.
|
**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
|
**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)
|
### 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)
|
### 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
|
## Task 1: Add `embed_image_secret` to WASM Crate
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-wasm/src/lib.rs`
|
- Modify: `crates/relicario-wasm/src/lib.rs`
|
||||||
|
|
||||||
- [ ] **Step 1: Write the test**
|
- [ ] **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
|
```rust
|
||||||
#[test]
|
#[test]
|
||||||
fn embed_then_extract_round_trip() {
|
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::codecs::jpeg::JpegEncoder;
|
||||||
use image::{ImageBuffer, ImageEncoder, Rgb};
|
use image::{ImageBuffer, ImageEncoder, Rgb};
|
||||||
|
|
||||||
@@ -81,12 +81,12 @@ fn embed_then_extract_round_trip() {
|
|||||||
|
|
||||||
- [ ] **Step 2: Run test to verify it fails**
|
- [ ] **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.
|
Expected: FAIL — `embed_image_secret` not defined.
|
||||||
|
|
||||||
- [ ] **Step 3: Add `image` dev-dependency to Cargo.toml**
|
- [ ] **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
|
```toml
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -96,7 +96,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
|||||||
|
|
||||||
- [ ] **Step 4: Implement the function**
|
- [ ] **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
|
```rust
|
||||||
/// Embed a 256-bit secret into a carrier JPEG image.
|
/// 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
|
let secret: [u8; 32] = secret
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
|
.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()))
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 5: Run test to verify it passes**
|
- [ ] **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
|
Expected: PASS
|
||||||
|
|
||||||
- [ ] **Step 6: Rebuild WASM**
|
- [ ] **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.
|
Expected: Builds successfully.
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
git commit -m "feat: add embed_image_secret to WASM crate"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ Create `extension/setup.html`:
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<style>
|
<style>
|
||||||
/* Override popup constraints for full-page layout */
|
/* Override popup constraints for full-page layout */
|
||||||
@@ -339,12 +339,12 @@ let state: WizardState = {
|
|||||||
|
|
||||||
// --- WASM ---
|
// --- WASM ---
|
||||||
|
|
||||||
type WasmModule = typeof import('idfoto-wasm');
|
type WasmModule = typeof import('relicario-wasm');
|
||||||
let wasm: WasmModule | null = null;
|
let wasm: WasmModule | null = null;
|
||||||
|
|
||||||
async function initWasm(): Promise<WasmModule> {
|
async function initWasm(): Promise<WasmModule> {
|
||||||
if (wasm) return 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();
|
await mod.default();
|
||||||
wasm = mod;
|
wasm = mod;
|
||||||
return mod;
|
return mod;
|
||||||
@@ -378,7 +378,7 @@ function render(): void {
|
|||||||
const stepNames = ['git host', 'connection', 'create vault', 'done'];
|
const stepNames = ['git host', 'connection', 'create vault', 'done'];
|
||||||
|
|
||||||
let html = `
|
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="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>
|
<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>
|
<ol>
|
||||||
<li>Log in to your Gitea instance</li>
|
<li>Log in to your Gitea instance</li>
|
||||||
<li>Click <code>+</code> → <code>New Repository</code></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>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>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>
|
<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>
|
<div class="label" style="margin-bottom:8px">GITHUB SETUP</div>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Go to <strong>github.com</strong> → <code>New Repository</code></li>
|
<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>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>Click <code>Generate new token</code></li>
|
||||||
<li>Select <strong>only</strong> the vault repository under "Repository access"</li>
|
<li>Select <strong>only</strong> the vault repository under "Repository access"</li>
|
||||||
@@ -534,7 +534,7 @@ function renderStep4(): string {
|
|||||||
</p>
|
</p>
|
||||||
` : `
|
` : `
|
||||||
<p class="secondary" style="font-size:11px;margin-bottom:8px">
|
<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>
|
</p>
|
||||||
<button class="btn" data-action="push-to-extension">Configure Extension</button>
|
<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="form-group" style="margin-top:16px">
|
||||||
<div class="label">EXTENSION SETUP</div>
|
<div class="label">EXTENSION SETUP</div>
|
||||||
<p class="secondary" style="font-size:11px;margin-bottom:8px">
|
<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>
|
</p>
|
||||||
<div class="config-blob" data-action="copy-config" title="Click to copy">
|
<div class="config-blob" data-action="copy-config" title="Click to copy">
|
||||||
${escapeHtml(JSON.stringify({
|
${escapeHtml(JSON.stringify({
|
||||||
@@ -796,9 +796,9 @@ async function createVault(): Promise<void> {
|
|||||||
const manifestEnc = w.encrypt_manifest(emptyManifest, masterKey);
|
const manifestEnc = w.encrypt_manifest(emptyManifest, masterKey);
|
||||||
|
|
||||||
// 7. Push vault files to repo
|
// 7. Push vault files to repo
|
||||||
await git.writeFile('.idfoto/salt', salt, 'feat: initialize idfoto vault');
|
await git.writeFile('.relicario/salt', salt, 'feat: initialize relicario vault');
|
||||||
await git.writeFile('.idfoto/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
|
await git.writeFile('.relicario/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/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
|
||||||
await git.writeFile('manifest.enc', new Uint8Array(manifestEnc), 'feat: add encrypted manifest');
|
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
|
```json
|
||||||
"web_accessible_resources": [{
|
"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>"]
|
"matches": ["<all_urls>"]
|
||||||
}]
|
}]
|
||||||
```
|
```
|
||||||
@@ -901,7 +901,7 @@ git commit -m "feat: add setup wizard to webpack build and extension manifest"
|
|||||||
- [ ] **Step 1: Rebuild WASM**
|
- [ ] **Step 1: Rebuild WASM**
|
||||||
|
|
||||||
```bash
|
```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**
|
- [ ] **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 2: enter real host/token/repo, test connection works
|
||||||
- Step 3: pick a JPEG, enter passphrase, create vault pushes files
|
- Step 3: pick a JPEG, enter passphrase, create vault pushes files
|
||||||
- Step 4: download reference image works, extension detection works
|
- 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
|
5. Open extension popup, unlock with passphrase — should work with the just-created vault
|
||||||
|
|
||||||
- [ ] **Step 5: Fix any issues found**
|
- [ ] **Step 5: Fix any issues found**
|
||||||
@@ -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.
|
> **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.
|
**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
|
**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)
|
### Rust (new crate)
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/idfoto-wasm/
|
crates/relicario-wasm/
|
||||||
├── Cargo.toml
|
├── Cargo.toml
|
||||||
└── src/
|
└── src/
|
||||||
└── lib.rs # wasm-bindgen wrappers + TOTP implementation
|
└── lib.rs # wasm-bindgen wrappers + TOTP implementation
|
||||||
@@ -26,8 +26,8 @@ crates/idfoto-wasm/
|
|||||||
### Rust (modified)
|
### Rust (modified)
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/idfoto-core/src/entry.rs # Add group field to Entry and ManifestEntry
|
crates/relicario-core/src/entry.rs # Add group field to Entry and ManifestEntry
|
||||||
Cargo.toml # Add idfoto-wasm to workspace members
|
Cargo.toml # Add relicario-wasm to workspace members
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extension (all new)
|
### Extension (all new)
|
||||||
@@ -73,13 +73,13 @@ extension/
|
|||||||
## Task 0: Add Heavy Comments to Existing Rust Code
|
## Task 0: Add Heavy Comments to Existing Rust Code
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-core/src/lib.rs`
|
- Modify: `crates/relicario-core/src/lib.rs`
|
||||||
- Modify: `crates/idfoto-core/src/error.rs`
|
- Modify: `crates/relicario-core/src/error.rs`
|
||||||
- Modify: `crates/idfoto-core/src/crypto.rs`
|
- Modify: `crates/relicario-core/src/crypto.rs`
|
||||||
- Modify: `crates/idfoto-core/src/entry.rs`
|
- Modify: `crates/relicario-core/src/entry.rs`
|
||||||
- Modify: `crates/idfoto-core/src/vault.rs`
|
- Modify: `crates/relicario-core/src/vault.rs`
|
||||||
- Modify: `crates/idfoto-core/src/imgsecret.rs`
|
- Modify: `crates/relicario-core/src/imgsecret.rs`
|
||||||
- Modify: `crates/idfoto-cli/src/main.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.
|
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`**
|
- [ ] **Step 1: Add module-level docs and comments to `lib.rs`**
|
||||||
|
|
||||||
```rust
|
```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,
|
//! This crate is deliberately bytes-in/bytes-out — no filesystem, no network,
|
||||||
//! no git operations. This makes it portable to WASM (browser extension),
|
//! no git operations. This makes it portable to WASM (browser extension),
|
||||||
@@ -174,7 +174,7 @@ Expected: All tests pass unchanged.
|
|||||||
- [ ] **Step 9: Commit**
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
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
|
## Task 1: Add `group` Field to Core Data Model
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/idfoto-core/src/entry.rs`
|
- Modify: `crates/relicario-core/src/entry.rs`
|
||||||
- Modify: `crates/idfoto-core/src/vault.rs` (test helpers)
|
- Modify: `crates/relicario-core/src/vault.rs` (test helpers)
|
||||||
- Modify: `crates/idfoto-cli/src/main.rs` (Entry construction sites)
|
- Modify: `crates/relicario-cli/src/main.rs` (Entry construction sites)
|
||||||
- Test: `crates/idfoto-core/src/entry.rs` (inline tests)
|
- Test: `crates/relicario-core/src/entry.rs` (inline tests)
|
||||||
|
|
||||||
- [ ] **Step 1: Add `group` field to `Entry` struct**
|
- [ ] **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
|
```rust
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
@@ -232,7 +232,7 @@ pub struct ManifestEntry {
|
|||||||
|
|
||||||
Update every place that constructs `Entry` or `ManifestEntry` to include `group: None`. These are:
|
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
|
```rust
|
||||||
// Every Entry construction gets:
|
// Every Entry construction gets:
|
||||||
@@ -242,7 +242,7 @@ group: None,
|
|||||||
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
|
```rust
|
||||||
// sample_entry() gets:
|
// sample_entry() gets:
|
||||||
@@ -252,7 +252,7 @@ group: None,
|
|||||||
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
|
```rust
|
||||||
// Entry construction gets:
|
// Entry construction gets:
|
||||||
@@ -262,7 +262,7 @@ group: None,
|
|||||||
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
|
```rust
|
||||||
// Every Entry construction gets:
|
// Every Entry construction gets:
|
||||||
@@ -274,7 +274,7 @@ group: None,
|
|||||||
|
|
||||||
- [ ] **Step 4: Add a test for backwards compatibility (deserialize without group)**
|
- [ ] **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
|
```rust
|
||||||
#[test]
|
#[test]
|
||||||
@@ -329,35 +329,35 @@ Expected: All tests pass, including new backwards-compatibility tests.
|
|||||||
- [ ] **Step 6: Commit**
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
```bash
|
```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"
|
git commit -m "feat: add group field to Entry and ManifestEntry"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 2: Create `idfoto-wasm` Crate
|
## Task 2: Create `relicario-wasm` Crate
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `crates/idfoto-wasm/Cargo.toml`
|
- Create: `crates/relicario-wasm/Cargo.toml`
|
||||||
- Create: `crates/idfoto-wasm/src/lib.rs`
|
- Create: `crates/relicario-wasm/src/lib.rs`
|
||||||
- Modify: `Cargo.toml` (workspace members)
|
- Modify: `Cargo.toml` (workspace members)
|
||||||
|
|
||||||
- [ ] **Step 1: Create Cargo.toml**
|
- [ ] **Step 1: Create Cargo.toml**
|
||||||
|
|
||||||
Create `crates/idfoto-wasm/Cargo.toml`:
|
Create `crates/relicario-wasm/Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[package]
|
[package]
|
||||||
name = "idfoto-wasm"
|
name = "relicario-wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "WASM bindings for idfoto password manager"
|
description = "WASM bindings for relicario password manager"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idfoto-core = { path = "../idfoto-core" }
|
relicario-core = { path = "../relicario-core" }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -371,21 +371,21 @@ wasm-bindgen-test = "0.3"
|
|||||||
|
|
||||||
- [ ] **Step 2: Add to workspace**
|
- [ ] **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
|
```toml
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/idfoto-core",
|
"crates/relicario-core",
|
||||||
"crates/idfoto-cli",
|
"crates/relicario-cli",
|
||||||
"crates/idfoto-wasm",
|
"crates/relicario-wasm",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Write the WASM wrapper**
|
- [ ] **Step 3: Write the WASM wrapper**
|
||||||
|
|
||||||
Create `crates/idfoto-wasm/src/lib.rs`:
|
Create `crates/relicario-wasm/src/lib.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
@@ -399,7 +399,7 @@ pub fn derive_master_key(
|
|||||||
salt: &[u8],
|
salt: &[u8],
|
||||||
params_json: &str,
|
params_json: &str,
|
||||||
) -> Result<Vec<u8>, JsValue> {
|
) -> 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()))?;
|
serde_json::from_str(params_json).map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
let image_secret: [u8; 32] = image_secret
|
let image_secret: [u8; 32] = image_secret
|
||||||
@@ -409,7 +409,7 @@ pub fn derive_master_key(
|
|||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("salt must be exactly 32 bytes"))?;
|
.map_err(|_| JsValue::from_str("salt must be exactly 32 bytes"))?;
|
||||||
|
|
||||||
let key = idfoto_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
let key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, ¶ms)
|
||||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
|
|
||||||
Ok(key.to_vec())
|
Ok(key.to_vec())
|
||||||
@@ -421,7 +421,7 @@ pub fn encrypt(plaintext: &[u8], key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|||||||
let key: [u8; 32] = key
|
let key: [u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.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.
|
/// 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
|
let key: [u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.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.
|
/// Extract a 256-bit secret from a JPEG with an embedded secret.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
|
pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
let secret =
|
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())
|
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
|
let key: [u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.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()))?;
|
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.
|
/// 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()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||||
let entry =
|
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()))
|
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
|
let key: [u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.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()))?;
|
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.
|
/// 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
|
let key: [u8; 32] = key
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
.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()))?;
|
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||||
serde_json::to_string(&manifest).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**
|
- [ ] **Step 4: Verify it compiles**
|
||||||
|
|
||||||
Run: `cargo build -p idfoto-wasm`
|
Run: `cargo build -p relicario-wasm`
|
||||||
Expected: Compiles successfully.
|
Expected: Compiles successfully.
|
||||||
|
|
||||||
- [ ] **Step 5: Run tests**
|
- [ ] **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.
|
Expected: All tests pass, including TOTP RFC 6238 test vectors.
|
||||||
|
|
||||||
- [ ] **Step 6: Test WASM compilation**
|
- [ ] **Step 6: Test WASM compilation**
|
||||||
|
|
||||||
Run: `cargo install wasm-pack` (if not already installed), then:
|
Run: `cargo install wasm-pack` (if not already installed), then:
|
||||||
```bash
|
```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**
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/idfoto-wasm/ Cargo.toml extension/wasm/
|
git add crates/relicario-wasm/ Cargo.toml extension/wasm/
|
||||||
git commit -m "feat: add idfoto-wasm crate with wasm-bindgen wrappers and TOTP"
|
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
|
```json
|
||||||
{
|
{
|
||||||
"name": "idfoto-extension",
|
"name": "relicario-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"dev": "webpack --mode development --watch",
|
"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"
|
"build:all": "npm run build:wasm && npm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -682,13 +682,13 @@ Note: `@anthropic-ai/sdk` is NOT needed — remove that. The devDependencies sho
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "idfoto-extension",
|
"name": "relicario-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"dev": "webpack --mode development --watch",
|
"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"
|
"build:all": "npm run build:wasm && npm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -706,13 +706,13 @@ Actually, strike that — no anthropic SDK. Final version:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "idfoto-extension",
|
"name": "relicario-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"dev": "webpack --mode development --watch",
|
"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"
|
"build:all": "npm run build:wasm && npm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -781,8 +781,8 @@ module.exports = {
|
|||||||
{ from: 'src/popup/index.html', to: 'popup.html' },
|
{ from: 'src/popup/index.html', to: 'popup.html' },
|
||||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||||
{ from: 'icons', to: 'icons' },
|
{ from: 'icons', to: 'icons' },
|
||||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||||
{ from: 'wasm/idfoto_wasm.js', to: '.' },
|
{ from: 'wasm/relicario_wasm.js', to: '.' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -797,7 +797,7 @@ module.exports = {
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"permissions": ["storage", "activeTab", "clipboardWrite"],
|
"permissions": ["storage", "activeTab", "clipboardWrite"],
|
||||||
@@ -838,7 +838,7 @@ Create `extension/src/popup/index.html`:
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=360">
|
<meta name="viewport" content="width=360">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>idfoto</title>
|
<title>relicario</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
@@ -1286,15 +1286,15 @@ import type { GitHost } from './git-host';
|
|||||||
import type { Entry, Manifest, ManifestEntry } from '../shared/types';
|
import type { Entry, Manifest, ManifestEntry } from '../shared/types';
|
||||||
|
|
||||||
// These will be set by the service worker index after WASM init
|
// 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) {
|
export function setWasm(w: typeof wasm) {
|
||||||
wasm = w;
|
wasm = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchVaultMeta(git: GitHost): Promise<{ salt: Uint8Array; paramsJson: string }> {
|
export async function fetchVaultMeta(git: GitHost): Promise<{ salt: Uint8Array; paramsJson: string }> {
|
||||||
const salt = await git.readFile('.idfoto/salt');
|
const salt = await git.readFile('.relicario/salt');
|
||||||
const paramsBytes = await git.readFile('.idfoto/params.json');
|
const paramsBytes = await git.readFile('.relicario/params.json');
|
||||||
const paramsJson = new TextDecoder().decode(paramsBytes);
|
const paramsJson = new TextDecoder().decode(paramsBytes);
|
||||||
return { salt, paramsJson };
|
return { salt, paramsJson };
|
||||||
}
|
}
|
||||||
@@ -1414,13 +1414,13 @@ import {
|
|||||||
let masterKey: Uint8Array | null = null;
|
let masterKey: Uint8Array | null = null;
|
||||||
let manifest: Manifest | null = null;
|
let manifest: Manifest | null = null;
|
||||||
let gitHost: GitHost | 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 ───────────────────────────────────────────────────
|
// ─── WASM initialization ───────────────────────────────────────────────────
|
||||||
|
|
||||||
async function initWasm(): Promise<typeof import('../../wasm/idfoto_wasm')> {
|
async function initWasm(): Promise<typeof import('../../wasm/relicario_wasm')> {
|
||||||
if (wasm) return 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();
|
await mod.default();
|
||||||
wasm = mod;
|
wasm = mod;
|
||||||
setWasm(mod);
|
setWasm(mod);
|
||||||
@@ -2118,7 +2118,7 @@ import { sendMessage, navigate } from '../popup';
|
|||||||
|
|
||||||
export function renderUnlock(container: HTMLElement) {
|
export function renderUnlock(container: HTMLElement) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="brand">idfoto</div>
|
<div class="brand">relicario</div>
|
||||||
<div style="margin-top: 16px">
|
<div style="margin-top: 16px">
|
||||||
<div class="label">PASSPHRASE</div>
|
<div class="label">PASSPHRASE</div>
|
||||||
<input type="password" id="passphrase" placeholder="Enter passphrase..." autofocus>
|
<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) {
|
export async function renderEntryList(container: HTMLElement) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="brand">idfoto</div>
|
<div class="brand">relicario</div>
|
||||||
<div class="status">🔓 unlocked</div>
|
<div class="status">🔓 unlocked</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
@@ -2702,7 +2702,7 @@ export function renderSetupWizard(container: HTMLElement) {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
container.innerHTML = `
|
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="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 class="progress-bar"><div class="progress-bar-fill" style="width: ${(step / 3) * 100}%"></div></div>
|
||||||
<div id="wizard-content"></div>
|
<div id="wizard-content"></div>
|
||||||
@@ -3128,10 +3128,10 @@ function showPicker(
|
|||||||
passwordField: HTMLInputElement
|
passwordField: HTMLInputElement
|
||||||
) {
|
) {
|
||||||
// Remove any existing picker
|
// 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');
|
const picker = document.createElement('div');
|
||||||
picker.className = 'idfoto-picker';
|
picker.className = 'relicario-picker';
|
||||||
picker.style.cssText = `
|
picker.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -3222,7 +3222,7 @@ git commit -m "feat: add content script with form detection, field icon, and aut
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build WASM
|
# 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
|
# Install deps and build extension
|
||||||
cd extension && npm install && npm run build
|
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**
|
- [ ] **Step 2: Note the WASM binary size**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls -lh extension/wasm/idfoto_wasm_bg.wasm
|
ls -lh extension/wasm/relicario_wasm_bg.wasm
|
||||||
ls -lh extension/dist/idfoto_wasm_bg.wasm
|
ls -lh extension/dist/relicario_wasm_bg.wasm
|
||||||
```
|
```
|
||||||
|
|
||||||
Record the size for reference. If >2 MB uncompressed, consider optimizing later.
|
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 |
|
| 0 | Add heavy comments to existing Rust code | None |
|
||||||
| 1 | Add `group` field to core data model | Task 0 |
|
| 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 |
|
| 3 | Extension scaffolding | Task 2 |
|
||||||
| 4 | Shared types and messages | Task 3 |
|
| 4 | Shared types and messages | Task 3 |
|
||||||
| 5 | Git API layer | Task 4 |
|
| 5 | Git API layer | Task 4 |
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
|||||||
# idfoto — Design Specification
|
# relicario — Design Specification
|
||||||
|
|
||||||
A git-backed, self-hostable password manager with a Rust core, CLI, and Chrome browser extension. The reference image as a DCT-embedded secret carrier is the core differentiator.
|
A git-backed, self-hostable password manager with a Rust core, CLI, and Chrome browser extension. The reference image as a DCT-embedded secret carrier is the core differentiator.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
idfoto is a password manager where vault decryption requires two independent factors: a passphrase the user memorizes and a reference JPEG that carries a 256-bit secret embedded via DCT steganography. The vault lives in a git repository (self-hosted on the user's own Gitea instance), and the server only ever sees opaque ciphertext. Compromise of either factor alone is insufficient to decrypt the vault.
|
relicario is a password manager where vault decryption requires two independent factors: a passphrase the user memorizes and a reference JPEG that carries a 256-bit secret embedded via DCT steganography. The vault lives in a git repository (self-hosted on the user's own Gitea instance), and the server only ever sees opaque ciphertext. Compromise of either factor alone is insufficient to decrypt the vault.
|
||||||
|
|
||||||
Primary goals: portfolio project for adlee.work, architectural elegance, legibility-as-security (the README should read as the security proof), learning Rust, and fun to tinker with.
|
Primary goals: portfolio project for adlee.work, architectural elegance, legibility-as-security (the README should read as the security proof), learning Rust, and fun to tinker with.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ A collection of credentials (usernames, passwords, URLs, TOTP seeds, notes) belo
|
|||||||
| Stolen device | Filesystem: reference image, device key, cached vault | Decrypt vault | Attacker has image_secret but not passphrase. Argon2id makes brute-force expensive. |
|
| Stolen device | Filesystem: reference image, device key, cached vault | Decrypt vault | Attacker has image_secret but not passphrase. Argon2id makes brute-force expensive. |
|
||||||
| Stolen device + weak passphrase | Same + feasible brute-force | Decrypt vault | Enforce minimum passphrase strength at vault creation. Universal worst case. |
|
| Stolen device + weak passphrase | Same + feasible brute-force | Decrypt vault | Enforce minimum passphrase strength at vault creation. Universal worst case. |
|
||||||
| Shoulder surfer | Observed passphrase | Decrypt vault (if they also get image) | Passphrase alone insufficient — still need image_secret. |
|
| Shoulder surfer | Observed passphrase | Decrypt vault (if they also get image) | Passphrase alone insufficient — still need image_secret. |
|
||||||
| Credential stuffing | Leaked email/password from other breaches | Access user's accounts | idfoto generates unique passwords per site. Breach of site A doesn't compromise site B. |
|
| Credential stuffing | Leaked email/password from other breaches | Access user's accounts | relicario generates unique passwords per site. Breach of site A doesn't compromise site B. |
|
||||||
|
|
||||||
### Out of scope
|
### Out of scope
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ passphrase (user types, UTF-8 encoded)
|
|||||||
▼
|
▼
|
||||||
Argon2id(
|
Argon2id(
|
||||||
password = passphrase_bytes || image_secret_bytes, // concatenated, 32-byte secret appended
|
password = passphrase_bytes || image_secret_bytes, // concatenated, 32-byte secret appended
|
||||||
salt = vault_salt, // 32 bytes, from .idfoto/salt
|
salt = vault_salt, // 32 bytes, from .relicario/salt
|
||||||
memory = 64 MiB,
|
memory = 64 MiB,
|
||||||
iterations = 3,
|
iterations = 3,
|
||||||
parallelism = 4,
|
parallelism = 4,
|
||||||
@@ -79,7 +79,7 @@ With a 4-word diceware passphrase (~51 bits) and Argon2id at 64 MiB, brute-force
|
|||||||
Compared to competitors:
|
Compared to competitors:
|
||||||
- LastPass/Bitwarden: server breach exposes ~40-60 bits (master password only)
|
- LastPass/Bitwarden: server breach exposes ~40-60 bits (master password only)
|
||||||
- 1Password: server breach exposes password + 128-bit Secret Key
|
- 1Password: server breach exposes password + 128-bit Secret Key
|
||||||
- idfoto: server breach exposes password + 256-bit image_secret
|
- relicario: server breach exposes password + 256-bit image_secret
|
||||||
|
|
||||||
### Authenticated encryption
|
### Authenticated encryption
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ Nonce is generated fresh (CSPRNG) on every write. Version byte allows future for
|
|||||||
|
|
||||||
### KDF parameters
|
### KDF parameters
|
||||||
|
|
||||||
Stored in `.idfoto/params.json` (plaintext, committed). Configurable per-vault:
|
Stored in `.relicario/params.json` (plaintext, committed). Configurable per-vault:
|
||||||
- Default: `argon2_m=65536` (64 MiB), `argon2_t=3`, `argon2_p=4`
|
- Default: `argon2_m=65536` (64 MiB), `argon2_t=3`, `argon2_p=4`
|
||||||
- Users can increase for CLI-only use on powerful hardware
|
- Users can increase for CLI-only use on powerful hardware
|
||||||
- Enables future parameter upgrades without format changes
|
- Enables future parameter upgrades without format changes
|
||||||
@@ -177,13 +177,13 @@ Caller must normalize EXIF orientation before passing JPEG to embed/extract. EXI
|
|||||||
## Vault Format & Repo Layout
|
## Vault Format & Repo Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
idfoto-vault/
|
relicario-vault/
|
||||||
├── manifest.enc # encrypted JSON: entry index, vault metadata
|
├── manifest.enc # encrypted JSON: entry index, vault metadata
|
||||||
├── entries/
|
├── entries/
|
||||||
│ ├── a1b2c3d4.enc # one encrypted entry per file, random hex ID
|
│ ├── a1b2c3d4.enc # one encrypted entry per file, random hex ID
|
||||||
│ ├── e5f6a7b8.enc
|
│ ├── e5f6a7b8.enc
|
||||||
│ └── ...
|
│ └── ...
|
||||||
└── .idfoto/
|
└── .relicario/
|
||||||
├── salt # 32 bytes, plaintext (prevents precomputation)
|
├── salt # 32 bytes, plaintext (prevents precomputation)
|
||||||
├── params.json # Argon2id parameters, plaintext
|
├── params.json # Argon2id parameters, plaintext
|
||||||
└── devices.json # authorized device ed25519 public keys, plaintext
|
└── devices.json # authorized device ed25519 public keys, plaintext
|
||||||
@@ -226,7 +226,7 @@ Flat schema. No nested objects, no folders, no tags for V1. Entry IDs are random
|
|||||||
|
|
||||||
### Plaintext metadata
|
### Plaintext metadata
|
||||||
|
|
||||||
Stored in `.idfoto/` and committed to the repo:
|
Stored in `.relicario/` and committed to the repo:
|
||||||
- `salt`: 32 random bytes, generated once at vault creation
|
- `salt`: 32 random bytes, generated once at vault creation
|
||||||
- `params.json`: Argon2id tuning knobs (memory, iterations, parallelism, format version)
|
- `params.json`: Argon2id tuning knobs (memory, iterations, parallelism, format version)
|
||||||
- `devices.json`: list of authorized device ed25519 public keys, used to verify commit signatures
|
- `devices.json`: list of authorized device ed25519 public keys, used to verify commit signatures
|
||||||
@@ -244,20 +244,20 @@ Preserved as-is. Every add/edit/rm is a commit. Provides "when was this password
|
|||||||
## Crate Layout
|
## Crate Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
idfoto/
|
relicario/
|
||||||
├── Cargo.toml # workspace root
|
├── Cargo.toml # workspace root
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── idfoto-core/ # library: imgsecret, KDF, vault format
|
│ ├── relicario-core/ # library: imgsecret, KDF, vault format
|
||||||
│ │ └── src/
|
│ │ └── src/
|
||||||
│ │ ├── lib.rs
|
│ │ ├── lib.rs
|
||||||
│ │ ├── imgsecret.rs
|
│ │ ├── imgsecret.rs
|
||||||
│ │ ├── kdf.rs
|
│ │ ├── kdf.rs
|
||||||
│ │ ├── vault.rs
|
│ │ ├── vault.rs
|
||||||
│ │ └── entry.rs
|
│ │ └── entry.rs
|
||||||
│ ├── idfoto-cli/ # binary: the `idfoto` CLI
|
│ ├── relicario-cli/ # binary: the `relicario` CLI
|
||||||
│ │ └── src/
|
│ │ └── src/
|
||||||
│ │ └── main.rs
|
│ │ └── main.rs
|
||||||
│ └── idfoto-wasm/ # wasm-bindgen wrapper around core
|
│ └── relicario-wasm/ # wasm-bindgen wrapper around core
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ └── lib.rs
|
│ └── lib.rs
|
||||||
├── extension/ # TypeScript Chrome MV3 extension
|
├── extension/ # TypeScript Chrome MV3 extension
|
||||||
@@ -271,14 +271,14 @@ idfoto/
|
|||||||
|
|
||||||
### Design principles
|
### Design principles
|
||||||
|
|
||||||
- **`idfoto-core` is platform-agnostic.** No filesystem, no git, no network. Takes bytes, returns bytes. This makes it trivially portable to WASM, Android (via JNI), iOS (via Swift bridge).
|
- **`relicario-core` is platform-agnostic.** No filesystem, no git, no network. Takes bytes, returns bytes. This makes it trivially portable to WASM, Android (via JNI), iOS (via Swift bridge).
|
||||||
- **`idfoto-cli`** is the platform layer. Handles filesystem, git operations (shells out to `git`), clipboard, terminal I/O.
|
- **`relicario-cli`** is the platform layer. Handles filesystem, git operations (shells out to `git`), clipboard, terminal I/O.
|
||||||
- **`idfoto-wasm`** is a thin wasm-bindgen wrapper exposing core functions to JavaScript.
|
- **`relicario-wasm`** is a thin wasm-bindgen wrapper exposing core functions to JavaScript.
|
||||||
- **`extension/`** is TypeScript/MV3. Loads the WASM module, runs crypto inline (no native messaging bridge).
|
- **`extension/`** is TypeScript/MV3. Loads the WASM module, runs crypto inline (no native messaging bridge).
|
||||||
|
|
||||||
### Rust crate dependencies (expected)
|
### Rust crate dependencies (expected)
|
||||||
|
|
||||||
**idfoto-core:**
|
**relicario-core:**
|
||||||
- `argon2` — Argon2id KDF
|
- `argon2` — Argon2id KDF
|
||||||
- `chacha20poly1305` — XChaCha20-Poly1305 AEAD
|
- `chacha20poly1305` — XChaCha20-Poly1305 AEAD
|
||||||
- `sha2` — SHA-256 for hashing
|
- `sha2` — SHA-256 for hashing
|
||||||
@@ -290,44 +290,44 @@ idfoto/
|
|||||||
- `ed25519-dalek` — device key signing (used by CLI, exposed via core)
|
- `ed25519-dalek` — device key signing (used by CLI, exposed via core)
|
||||||
- `thiserror` — error types
|
- `thiserror` — error types
|
||||||
|
|
||||||
**idfoto-cli:**
|
**relicario-cli:**
|
||||||
- `clap` (derive) — argument parsing
|
- `clap` (derive) — argument parsing
|
||||||
- `anyhow` — CLI error handling
|
- `anyhow` — CLI error handling
|
||||||
- `rpassword` — passphrase prompt without echo
|
- `rpassword` — passphrase prompt without echo
|
||||||
- `arboard` or `cli-clipboard` — clipboard access
|
- `arboard` or `cli-clipboard` — clipboard access
|
||||||
- `dirs` — platform config/data directories
|
- `dirs` — platform config/data directories
|
||||||
|
|
||||||
**idfoto-wasm:**
|
**relicario-wasm:**
|
||||||
- `wasm-bindgen` — JS interop
|
- `wasm-bindgen` — JS interop
|
||||||
- `js-sys`, `web-sys` — browser APIs
|
- `js-sys`, `web-sys` — browser APIs
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
idfoto init # Create vault: generate salt, prompt for passphrase,
|
relicario init # Create vault: generate salt, prompt for passphrase,
|
||||||
# prompt for carrier image, embed image_secret,
|
# prompt for carrier image, embed image_secret,
|
||||||
# output reference JPEG, git init + first commit
|
# output reference JPEG, git init + first commit
|
||||||
|
|
||||||
idfoto add # Prompt for entry fields, encrypt, commit
|
relicario add # Prompt for entry fields, encrypt, commit
|
||||||
idfoto get <name> # Case-insensitive substring match on name/URL, decrypt, copy password to clipboard (30s TTL)
|
relicario get <name> # Case-insensitive substring match on name/URL, decrypt, copy password to clipboard (30s TTL)
|
||||||
idfoto list # Decrypt manifest, print entry names/URLs
|
relicario list # Decrypt manifest, print entry names/URLs
|
||||||
idfoto edit <name> # Decrypt entry, prompt for changes, re-encrypt, commit
|
relicario edit <name> # Decrypt entry, prompt for changes, re-encrypt, commit
|
||||||
idfoto rm <name> # Remove entry file, update manifest, commit
|
relicario rm <name> # Remove entry file, update manifest, commit
|
||||||
idfoto sync # git pull --rebase && git push
|
relicario sync # git pull --rebase && git push
|
||||||
idfoto generate # Generate a random password (utility, no vault interaction)
|
relicario generate # Generate a random password (utility, no vault interaction)
|
||||||
|
|
||||||
idfoto device add # Generate ed25519 keypair, add pubkey to devices.json, commit
|
relicario device add # Generate ed25519 keypair, add pubkey to devices.json, commit
|
||||||
idfoto device list # List authorized devices
|
relicario device list # List authorized devices
|
||||||
idfoto device revoke <name> # Remove device from devices.json, commit
|
relicario device revoke <name> # Remove device from devices.json, commit
|
||||||
```
|
```
|
||||||
|
|
||||||
Unlock flow: on any command that needs the vault, the CLI prompts for the passphrase and the reference image path (or uses a configured default path). Derives master_key, holds it in memory for the duration of the command, then drops it. No persistent daemon for V1 — each invocation re-derives.
|
Unlock flow: on any command that needs the vault, the CLI prompts for the passphrase and the reference image path (or uses a configured default path). Derives master_key, holds it in memory for the duration of the command, then drops it. No persistent daemon for V1 — each invocation re-derives.
|
||||||
|
|
||||||
Future: `idfoto unlock` could spawn a background agent (ssh-agent-style) that holds the key for a configurable TTL, so subsequent commands don't re-prompt.
|
Future: `relicario unlock` could spawn a background agent (ssh-agent-style) that holds the key for a configurable TTL, so subsequent commands don't re-prompt.
|
||||||
|
|
||||||
## Chrome Extension Architecture
|
## Chrome Extension Architecture
|
||||||
|
|
||||||
The Chrome MV3 extension loads `idfoto-wasm` directly — no native messaging bridge.
|
The Chrome MV3 extension loads `relicario-wasm` directly — no native messaging bridge.
|
||||||
|
|
||||||
- **Service worker:** initializes the WASM module, holds the master_key in memory after unlock, handles vault operations
|
- **Service worker:** initializes the WASM module, holds the master_key in memory after unlock, handles vault operations
|
||||||
- **Popup:** passphrase prompt, entry list/search, entry detail view
|
- **Popup:** passphrase prompt, entry list/search, entry detail view
|
||||||
@@ -343,16 +343,16 @@ Extension design details (popup UI, content script heuristics, autofill flow) ar
|
|||||||
|
|
||||||
Not in V1 scope. Planned approach:
|
Not in V1 scope. Planned approach:
|
||||||
|
|
||||||
- `idfoto export-recovery` generates a small encrypted file containing only the `image_secret` (32 bytes + metadata), locked with the passphrase alone (separate Argon2id derivation)
|
- `relicario export-recovery` generates a small encrypted file containing only the `image_secret` (32 bytes + metadata), locked with the passphrase alone (separate Argon2id derivation)
|
||||||
- User stores this file offline (USB drive, printed QR, safe deposit box)
|
- User stores this file offline (USB drive, printed QR, safe deposit box)
|
||||||
- Recovery: `idfoto recover --file recovery.enc` + passphrase → recovers image_secret → can decrypt vault from git
|
- Recovery: `relicario recover --file recovery.enc` + passphrase → recovers image_secret → can decrypt vault from git
|
||||||
- This is a second backup path alongside the "dead drop" reference JPEG (which can live on social media, personal website, etc.)
|
- This is a second backup path alongside the "dead drop" reference JPEG (which can live on social media, personal website, etc.)
|
||||||
|
|
||||||
## Post-V1 Ideas
|
## Post-V1 Ideas
|
||||||
|
|
||||||
- **Secure notes:** free-form encrypted text entries (no URL/username/password schema, just a title + body). Same encryption, same repo layout — just a different entry type field.
|
- **Secure notes:** free-form encrypted text entries (no URL/username/password schema, just a title + body). Same encryption, same repo layout — just a different entry type field.
|
||||||
- **Secure document storage:** encrypted file attachments up to 5-10 MB per entry. Stored as separate `.enc` blobs in an `attachments/` directory, referenced by entry ID. Git handles large binary blobs tolerably at this scale; git-lfs is an option if vaults grow beyond ~100 MB total.
|
- **Secure document storage:** encrypted file attachments up to 5-10 MB per entry. Stored as separate `.enc` blobs in an `attachments/` directory, referenced by entry ID. Git handles large binary blobs tolerably at this scale; git-lfs is an option if vaults grow beyond ~100 MB total.
|
||||||
- **`idfoto unlock` daemon:** ssh-agent-style background process that holds master_key for a configurable TTL, so repeated CLI commands don't re-prompt for passphrase.
|
- **`relicario unlock` daemon:** ssh-agent-style background process that holds master_key for a configurable TTL, so repeated CLI commands don't re-prompt for passphrase.
|
||||||
- **Mobile clients (Android/iOS):** Rust core compiles to ARM. Thin native wrappers (Kotlin/Swift) deferred.
|
- **Mobile clients (Android/iOS):** Rust core compiles to ARM. Thin native wrappers (Kotlin/Swift) deferred.
|
||||||
- **Import from LastPass/Bitwarden/1Password**
|
- **Import from LastPass/Bitwarden/1Password**
|
||||||
- **Firefox/Safari extensions**
|
- **Firefox/Safari extensions**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# idfoto — Credential Capture Design
|
# relicario — Credential Capture Design
|
||||||
|
|
||||||
Experimental feature that detects login form submissions and prompts the user to save or update credentials in the vault. Configurable prompt style (notification bar or toast). Off by default.
|
Experimental feature that detects login form submissions and prompts the user to save or update credentials in the vault. Configurable prompt style (notification bar or toast). Off by default.
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ A fixed-position bar at the top of the page, injected into the DOM:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
│ idfoto: Save login for github.com? (alee) [Save] [Never] [✕] │
|
│ relicario: Save login for github.com? (alee) [Save] [Never] [✕] │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ A floating element in the bottom-right corner:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────┐
|
┌─────────────────────────────────┐
|
||||||
│ idfoto │
|
│ relicario │
|
||||||
│ Save login for github.com? │
|
│ Save login for github.com? │
|
||||||
│ alee │
|
│ alee │
|
||||||
│ [Save] [Never] [✕] │
|
│ [Save] [Never] [✕] │
|
||||||
@@ -103,7 +103,7 @@ When user clicks:
|
|||||||
Stored in `chrome.storage.local` under key `settings`:
|
Stored in `chrome.storage.local` under key `settings`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface IdfotoSettings {
|
interface RelicarioSettings {
|
||||||
captureEnabled: boolean; // default: false
|
captureEnabled: boolean; // default: false
|
||||||
captureStyle: 'bar' | 'toast'; // default: 'bar'
|
captureStyle: 'bar' | 'toast'; // default: 'bar'
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ The toggle and style selector write to `chrome.storage.local`. Blacklist entries
|
|||||||
| { type: 'check_credential'; url: string; username: string; password: string }
|
| { type: 'check_credential'; url: string; username: string; password: string }
|
||||||
| { type: 'blacklist_site'; hostname: string }
|
| { type: 'blacklist_site'; hostname: string }
|
||||||
| { type: 'get_settings' }
|
| { type: 'get_settings' }
|
||||||
| { type: 'update_settings'; settings: Partial<IdfotoSettings> }
|
| { type: 'update_settings'; settings: Partial<RelicarioSettings> }
|
||||||
| { type: 'get_blacklist' }
|
| { type: 'get_blacklist' }
|
||||||
| { type: 'remove_blacklist'; hostname: string }
|
| { type: 'remove_blacklist'; hostname: string }
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ extension/src/popup/components/settings.ts # Settings view
|
|||||||
extension/src/content/detector.ts # Import and init capture module
|
extension/src/content/detector.ts # Import and init capture module
|
||||||
extension/src/service-worker/index.ts # Handle new message types
|
extension/src/service-worker/index.ts # Handle new message types
|
||||||
extension/src/shared/messages.ts # Add new Request/Response types
|
extension/src/shared/messages.ts # Add new Request/Response types
|
||||||
extension/src/shared/types.ts # Add IdfotoSettings interface
|
extension/src/shared/types.ts # Add RelicarioSettings interface
|
||||||
extension/src/popup/popup.ts # Add 'settings' view to state machine
|
extension/src/popup/popup.ts # Add 'settings' view to state machine
|
||||||
extension/src/popup/components/unlock.ts # Wire up settings button
|
extension/src/popup/components/unlock.ts # Wire up settings button
|
||||||
```
|
```
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# idfoto — Firefox Extension Port Design
|
# relicario — Firefox Extension Port Design
|
||||||
|
|
||||||
Port the existing Chrome MV3 extension to Firefox. Shared TypeScript source, separate manifests, separate build outputs. No code changes to components, popup, or content script.
|
Port the existing Chrome MV3 extension to Firefox. Shared TypeScript source, separate manifests, separate build outputs. No code changes to components, popup, or content script.
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ Firefox supports the `chrome.*` namespace for WebExtension APIs, so no `browser.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "idfoto@adlee.work",
|
"id": "relicario@adlee.work",
|
||||||
"strict_min_version": "128.0"
|
"strict_min_version": "128.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -71,7 +71,7 @@ Firefox supports the `chrome.*` namespace for WebExtension APIs, so no `browser.
|
|||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||||
},
|
},
|
||||||
"web_accessible_resources": [{
|
"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"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -94,12 +94,12 @@ async function initWasm(): Promise<WasmModule> {
|
|||||||
|
|
||||||
if (typeof ServiceWorkerGlobalScope !== 'undefined') {
|
if (typeof ServiceWorkerGlobalScope !== 'undefined') {
|
||||||
// Chrome MV3: service worker context — use initSync
|
// Chrome MV3: service worker context — use initSync
|
||||||
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();
|
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||||
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
||||||
} else {
|
} else {
|
||||||
// Firefox: background script context — dynamic import works
|
// Firefox: background script context — dynamic import works
|
||||||
const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm');
|
const wasmUrl = chrome.runtime.getURL('relicario_wasm_bg.wasm');
|
||||||
await initDefault(wasmUrl);
|
await initDefault(wasmUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ Identical to `webpack.config.js` except:
|
|||||||
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
|
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"dev:firefox": "webpack --config webpack.firefox.config.js --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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# idfoto — Standalone Vault Initialization Wizard Design
|
# relicario — Standalone Vault Initialization Wizard Design
|
||||||
|
|
||||||
A browser-based wizard that guides new users through creating an idfoto vault from scratch. Lives at `extension/setup.html`, uses the same WASM module as the extension, same terminal dark aesthetic. No server, no Rust toolchain required.
|
A browser-based wizard that guides new users through creating an relicario vault from scratch. Lives at `extension/setup.html`, uses the same WASM module as the extension, same terminal dark aesthetic. No server, no Rust toolchain required.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Step includes a "Next" button. No validation needed at this step.
|
|||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
- Host URL (e.g. `https://git.adlee.work` or `https://github.com`) — pre-filled based on host type selection
|
- Host URL (e.g. `https://git.adlee.work` or `https://github.com`) — pre-filled based on host type selection
|
||||||
- Repository path (e.g. `alee/idfoto-vault`)
|
- Repository path (e.g. `alee/relicario-vault`)
|
||||||
- API token (password field)
|
- API token (password field)
|
||||||
|
|
||||||
"Test Connection" button:
|
"Test Connection" button:
|
||||||
@@ -58,15 +58,15 @@ Two inputs:
|
|||||||
|
|
||||||
1. Load WASM module
|
1. Load WASM module
|
||||||
2. Generate random 32-byte `image_secret` via `crypto.getRandomValues()`
|
2. Generate random 32-byte `image_secret` via `crypto.getRandomValues()`
|
||||||
3. Embed secret into carrier JPEG via WASM `extract_image_secret` — wait, that's extract. We need `embed`. Check: the WASM crate currently only exposes `extract_image_secret`, not `embed`. **We need to add a `embed_image_secret` function to `idfoto-wasm`.**
|
3. Embed secret into carrier JPEG via WASM `extract_image_secret` — wait, that's extract. We need `embed`. Check: the WASM crate currently only exposes `extract_image_secret`, not `embed`. **We need to add a `embed_image_secret` function to `relicario-wasm`.**
|
||||||
4. Generate random 32-byte `salt` via `crypto.getRandomValues()`
|
4. Generate random 32-byte `salt` via `crypto.getRandomValues()`
|
||||||
5. Create `params.json` with default KDF params (`{"argon2_m":65536,"argon2_t":3,"argon2_p":4}`)
|
5. Create `params.json` with default KDF params (`{"argon2_m":65536,"argon2_t":3,"argon2_p":4}`)
|
||||||
6. Derive `master_key` via WASM `derive_master_key(passphrase, image_secret, salt, params_json)`
|
6. Derive `master_key` via WASM `derive_master_key(passphrase, image_secret, salt, params_json)`
|
||||||
7. Encrypt empty manifest (`{"entries":{},"version":1}`) via WASM `encrypt_manifest`
|
7. Encrypt empty manifest (`{"entries":{},"version":1}`) via WASM `encrypt_manifest`
|
||||||
8. Push files to repo via git API:
|
8. Push files to repo via git API:
|
||||||
- `.idfoto/salt` (raw 32 bytes)
|
- `.relicario/salt` (raw 32 bytes)
|
||||||
- `.idfoto/params.json` (JSON string)
|
- `.relicario/params.json` (JSON string)
|
||||||
- `.idfoto/devices.json` (`[]`)
|
- `.relicario/devices.json` (`[]`)
|
||||||
- `manifest.enc` (encrypted manifest bytes)
|
- `manifest.enc` (encrypted manifest bytes)
|
||||||
9. Show progress bar during push operations
|
9. Show progress bar during push operations
|
||||||
|
|
||||||
@@ -81,20 +81,20 @@ Two things happen:
|
|||||||
- Show warning: "Keep this image safe. You need it alongside your passphrase to unlock the vault. Store it somewhere you won't lose it."
|
- Show warning: "Keep this image safe. You need it alongside your passphrase to unlock the vault. Store it somewhere you won't lose it."
|
||||||
|
|
||||||
**Push config to extension (if available):**
|
**Push config to extension (if available):**
|
||||||
- Try to detect the idfoto extension via `chrome.runtime.sendMessage` with a `get_setup_state` message
|
- Try to detect the relicario extension via `chrome.runtime.sendMessage` with a `get_setup_state` message
|
||||||
- If extension responds: push `save_setup` message with `{ config: { hostType, hostUrl, repoPath, apiToken }, imageBase64 }`. Show "Extension configured! You can now open the extension and unlock your vault."
|
- If extension responds: push `save_setup` message with `{ config: { hostType, hostUrl, repoPath, apiToken }, imageBase64 }`. Show "Extension configured! You can now open the extension and unlock your vault."
|
||||||
- If extension not detected: show the config as a copyable JSON blob with instructions: "Install the idfoto extension, then paste this into the setup wizard." (Or just tell them to run through the extension setup manually with the same host/token/repo.)
|
- If extension not detected: show the config as a copyable JSON blob with instructions: "Install the relicario extension, then paste this into the setup wizard." (Or just tell them to run through the extension setup manually with the same host/token/repo.)
|
||||||
|
|
||||||
## WASM Crate Change
|
## WASM Crate Change
|
||||||
|
|
||||||
The `idfoto-wasm` crate needs one new function:
|
The `relicario-wasm` crate needs one new function:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsValue>
|
pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsValue>
|
||||||
```
|
```
|
||||||
|
|
||||||
This wraps `idfoto_core::imgsecret::embed`. Currently only `extract_image_secret` is exposed.
|
This wraps `relicario_core::imgsecret::embed`. Currently only `extract_image_secret` is exposed.
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ Add `setup.html` to the extension so it can be opened as a chrome-extension page
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"web_accessible_resources": [{
|
"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>"]
|
"matches": ["<all_urls>"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# idfoto — WASM + Chrome MV3 Extension Design
|
# relicario — WASM + Chrome MV3 Extension Design
|
||||||
|
|
||||||
The browser extension for idfoto. Compiles `idfoto-core` to WASM, wraps it in a Chrome MV3 extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access. No CLI dependency, no native messaging bridge.
|
The browser extension for relicario. Compiles `relicario-core` to WASM, wraps it in a Chrome MV3 extension with a terminal-aesthetic popup, conservative autofill, and direct Gitea/GitHub API access. No CLI dependency, no native messaging bridge.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- `idfoto-wasm` crate — wasm-bindgen wrapper around `idfoto-core`
|
- `relicario-wasm` crate — wasm-bindgen wrapper around `relicario-core`
|
||||||
- Chrome MV3 extension:
|
- Chrome MV3 extension:
|
||||||
- One-time setup wizard (git host + token + repo + reference image)
|
- One-time setup wizard (git host + token + repo + reference image)
|
||||||
- Service worker — WASM runtime, master_key holder, vault operations, git API
|
- Service worker — WASM runtime, master_key holder, vault operations, git API
|
||||||
@@ -44,9 +44,9 @@ pub struct ManifestEntry {
|
|||||||
|
|
||||||
The `group` field is a free-form string. No predefined list, no nesting. User types "work" or "family" and entries cluster. Backwards-compatible — existing vaults without `group` deserialize as `None` (ungrouped).
|
The `group` field is a free-form string. No predefined list, no nesting. User types "work" or "family" and entries cluster. Backwards-compatible — existing vaults without `group` deserialize as `None` (ungrouped).
|
||||||
|
|
||||||
## WASM Crate (`idfoto-wasm`)
|
## WASM Crate (`relicario-wasm`)
|
||||||
|
|
||||||
Thin wasm-bindgen wrapper exposing `idfoto-core` functions to JavaScript. Lives at `crates/idfoto-wasm/`.
|
Thin wasm-bindgen wrapper exposing `relicario-core` functions to JavaScript. Lives at `crates/relicario-wasm/`.
|
||||||
|
|
||||||
### Public API
|
### Public API
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ pub fn generate_entry_id() -> String
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idfoto-core = { path = "../idfoto-core" }
|
relicario-core = { path = "../relicario-core" }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -106,10 +106,10 @@ data-encoding = "2" # base32 decoding for TOTP secrets
|
|||||||
### WASM build
|
### WASM build
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: `idfoto_wasm.js` (JS glue) + `idfoto_wasm_bg.wasm` (binary). Expected size ~200-500 KB gzipped. The `image` crate's JPEG decoder is the heaviest component — optimize only if measured size is a problem.
|
Output: `relicario_wasm.js` (JS glue) + `relicario_wasm_bg.wasm` (binary). Expected size ~200-500 KB gzipped. The `image` crate's JPEG decoder is the heaviest component — optimize only if measured size is a problem.
|
||||||
|
|
||||||
### TOTP implementation
|
### TOTP implementation
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ interface WorkerState {
|
|||||||
interface VaultConfig {
|
interface VaultConfig {
|
||||||
hostType: "gitea" | "github";
|
hostType: "gitea" | "github";
|
||||||
hostUrl: string; // e.g. "https://git.adlee.work"
|
hostUrl: string; // e.g. "https://git.adlee.work"
|
||||||
repoPath: string; // e.g. "alee/idfoto-vault"
|
repoPath: string; // e.g. "alee/relicario-vault"
|
||||||
apiToken: string; // personal access token
|
apiToken: string; // personal access token
|
||||||
imageBytes: Uint8Array; // reference JPEG, stored in chrome.storage.local
|
imageBytes: Uint8Array; // reference JPEG, stored in chrome.storage.local
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ Popup and content script communicate with the service worker via typed messages:
|
|||||||
2. Popup sends `{ type: "unlock", passphrase }` to service worker
|
2. Popup sends `{ type: "unlock", passphrase }` to service worker
|
||||||
3. Service worker loads vault config from `chrome.storage.local` (includes image bytes)
|
3. Service worker loads vault config from `chrome.storage.local` (includes image bytes)
|
||||||
4. WASM: `extract_image_secret(image_bytes)` → `image_secret`
|
4. WASM: `extract_image_secret(image_bytes)` → `image_secret`
|
||||||
5. Service worker fetches `.idfoto/salt` and `.idfoto/params.json` via git API
|
5. Service worker fetches `.relicario/salt` and `.relicario/params.json` via git API
|
||||||
6. WASM: `derive_master_key(passphrase, image_secret, salt, params)` → `master_key`
|
6. WASM: `derive_master_key(passphrase, image_secret, salt, params)` → `master_key`
|
||||||
7. Service worker fetches `manifest.enc` via git API
|
7. Service worker fetches `manifest.enc` via git API
|
||||||
8. WASM: `decrypt_manifest(manifest_enc, master_key)` → manifest
|
8. WASM: `decrypt_manifest(manifest_enc, master_key)` → manifest
|
||||||
@@ -330,7 +330,7 @@ No shadow DOM traversal. No heuristic scoring. No iframe inspection. If the form
|
|||||||
### 2. Field Icon Injection
|
### 2. Field Icon Injection
|
||||||
|
|
||||||
When a password field is detected:
|
When a password field is detected:
|
||||||
- Small idfoto icon (16x16, inline SVG) appears at the right edge of the password field
|
- Small relicario icon (16x16, inline SVG) appears at the right edge of the password field
|
||||||
- Click triggers: send page URL to service worker → get matching entries
|
- Click triggers: send page URL to service worker → get matching entries
|
||||||
- Single match: fill immediately
|
- Single match: fill immediately
|
||||||
- Multiple matches: show inline picker (small dropdown below the icon)
|
- Multiple matches: show inline picker (small dropdown below the icon)
|
||||||
@@ -380,7 +380,7 @@ extension/
|
|||||||
│ └── shared/
|
│ └── shared/
|
||||||
│ ├── messages.ts # typed message definitions
|
│ ├── messages.ts # typed message definitions
|
||||||
│ └── types.ts # Entry, ManifestEntry, VaultConfig, etc.
|
│ └── types.ts # Entry, ManifestEntry, VaultConfig, etc.
|
||||||
├── wasm/ # wasm-pack output (idfoto_wasm.js + .wasm)
|
├── wasm/ # wasm-pack output (relicario_wasm.js + .wasm)
|
||||||
├── icons/ # extension icons (16, 48, 128px)
|
├── icons/ # extension icons (16, 48, 128px)
|
||||||
└── dist/ # build output → load unpacked into Chrome
|
└── dist/ # build output → load unpacked into Chrome
|
||||||
```
|
```
|
||||||
@@ -392,7 +392,7 @@ No framework. Vanilla TypeScript + DOM manipulation. The popup is small enough t
|
|||||||
### WASM build
|
### WASM build
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extension build
|
### Extension build
|
||||||
@@ -414,7 +414,7 @@ Chains wasm-pack then webpack. Dev mode: `npm run dev` watches TypeScript and au
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"permissions": ["storage", "activeTab", "clipboardWrite"],
|
"permissions": ["storage", "activeTab", "clipboardWrite"],
|
||||||
@@ -476,7 +476,7 @@ The token is stored in `chrome.storage.local`, which is sandboxed per-extension
|
|||||||
|
|
||||||
- Unit tests: each wrapper function round-trips correctly (`wasm-pack test --node`)
|
- Unit tests: each wrapper function round-trips correctly (`wasm-pack test --node`)
|
||||||
- TOTP: test vectors from RFC 6238 appendix B
|
- TOTP: test vectors from RFC 6238 appendix B
|
||||||
- Integration: derive key + encrypt + decrypt cycle matches `idfoto-core` output
|
- Integration: derive key + encrypt + decrypt cycle matches `relicario-core` output
|
||||||
|
|
||||||
### Extension (manual for V1)
|
### Extension (manual for V1)
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# idfoto — Typed Item Data Model Design
|
# relicario — Typed Item Data Model Design
|
||||||
|
|
||||||
Foundational data-model rewrite for idfoto. Replaces the single `Entry` type with a polymorphic typed-item system supporting Login, SecureNote, Identity, Card, Key, Document, and TOTP — with sections, custom fields, attachments, password history, soft-delete, and the security architecture needed to support 1Password-style daily-driver UX.
|
Foundational data-model rewrite for relicario. Replaces the single `Entry` type with a polymorphic typed-item system supporting Login, SecureNote, Identity, Card, Key, Document, and TOTP — with sections, custom fields, attachments, password history, soft-delete, and the security architecture needed to support 1Password-style daily-driver UX.
|
||||||
|
|
||||||
This is **Phase 1** of the broader 1Password-parity roadmap. Phase 0 (audit remediation) is the precursor implementation pass; Phase 2+ (admin portal, importers, Watchtower checks, etc.) build on top of this model.
|
This is **Phase 1** of the broader 1Password-parity roadmap. Phase 0 (audit remediation) is the precursor implementation pass; Phase 2+ (admin portal, importers, Watchtower checks, etc.) build on top of this model.
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ This is **Phase 1** of the broader 1Password-parity roadmap. Phase 0 (audit reme
|
|||||||
|
|
||||||
In:
|
In:
|
||||||
|
|
||||||
- New typed-item Rust data model in `idfoto-core` (replaces `Entry`)
|
- New typed-item Rust data model in `relicario-core` (replaces `Entry`)
|
||||||
- New on-disk repo layout (items + attachments split, settings file, format version 2)
|
- New on-disk repo layout (items + attachments split, settings file, format version 2)
|
||||||
- Cryptographic envelope updates (length-prefixed Argon2 inputs, Zeroize discipline, opaque session-handle WASM bridge)
|
- Cryptographic envelope updates (length-prefixed Argon2 inputs, Zeroize discipline, opaque session-handle WASM bridge)
|
||||||
- Security architecture for the extension boundary (split message router, origin-checked autofill, closed Shadow DOM rendering, hardened CLI git shell-out)
|
- Security architecture for the extension boundary (split message router, origin-checked autofill, closed Shadow DOM rendering, hardened CLI git shell-out)
|
||||||
@@ -69,7 +69,7 @@ Captured during brainstorming so the rationale is preserved:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌────────────────────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────────────────────┐
|
||||||
│ idfoto-core (Rust) │
|
│ relicario-core (Rust) │
|
||||||
│ - Item, ItemCore (7 variants), Field, Section, Attachment │
|
│ - Item, ItemCore (7 variants), Field, Section, Attachment │
|
||||||
│ - Manifest, VaultSettings │
|
│ - Manifest, VaultSettings │
|
||||||
│ - crypto: KDF (length-prefixed), AEAD, Zeroize discipline │
|
│ - crypto: KDF (length-prefixed), AEAD, Zeroize discipline │
|
||||||
@@ -79,7 +79,7 @@ Captured during brainstorming so the rationale is preserved:
|
|||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
┌──────────────────────┐ ┌──────────────────────────────────┐
|
┌──────────────────────┐ ┌──────────────────────────────────┐
|
||||||
│ idfoto-cli (Rust) │ │ idfoto-wasm (Rust → WASM) │
|
│ relicario-cli (Rust) │ │ relicario-wasm (Rust → WASM) │
|
||||||
│ - clap commands │ │ - opaque session handles │
|
│ - clap commands │ │ - opaque session handles │
|
||||||
│ - hardened git │ │ - typed-item API surface │
|
│ - hardened git │ │ - typed-item API surface │
|
||||||
│ - rpassword 7.x │ │ - master_key never returned to │
|
│ - rpassword 7.x │ │ - master_key never returned to │
|
||||||
@@ -141,7 +141,7 @@ pub enum ItemCore {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Each variant struct lives in `crates/idfoto-core/src/item_types/<type>.rs`. Compiler enforces exhaustiveness across the codebase — adding a new variant later means: create the file, add the enum variant, fix the (typically ~5) match-arm sites the compiler points at, register the UI form. No reflection, no registry, no runtime dispatch.
|
Each variant struct lives in `crates/relicario-core/src/item_types/<type>.rs`. Compiler enforces exhaustiveness across the codebase — adding a new variant later means: create the file, add the enum variant, fix the (typically ~5) match-arm sites the compiler points at, register the UI form. No reflection, no registry, no runtime dispatch.
|
||||||
|
|
||||||
### Per-type cores
|
### Per-type cores
|
||||||
|
|
||||||
@@ -296,14 +296,14 @@ pub enum SymbolCharset {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Single canonical generator implementation in `idfoto-core`, exposed via WASM and used by CLI directly. Both paths use `getrandom`-backed `OsRng` and `rand::distributions::Uniform` for unbiased sampling.
|
Single canonical generator implementation in `relicario-core`, exposed via WASM and used by CLI directly. Both paths use `getrandom`-backed `OsRng` and `rand::distributions::Uniform` for unbiased sampling.
|
||||||
|
|
||||||
## Storage, Manifest & Sync
|
## Storage, Manifest & Sync
|
||||||
|
|
||||||
### Repo layout
|
### Repo layout
|
||||||
|
|
||||||
```
|
```
|
||||||
.idfoto/
|
.relicario/
|
||||||
salt # 32-byte vault salt (KDF input)
|
salt # 32-byte vault salt (KDF input)
|
||||||
params.json # Argon2id parameters, format version
|
params.json # Argon2id parameters, format version
|
||||||
devices.json # authorized device ed25519 pubkeys
|
devices.json # authorized device ed25519 pubkeys
|
||||||
@@ -405,7 +405,7 @@ pub fn derive_master_key(
|
|||||||
image_secret: &[u8; 32],
|
image_secret: &[u8; 32],
|
||||||
salt: &[u8; 32],
|
salt: &[u8; 32],
|
||||||
params: &Argon2Params,
|
params: &Argon2Params,
|
||||||
) -> Result<Zeroizing<[u8; 32]>, IdfotoError> {
|
) -> Result<Zeroizing<[u8; 32]>, RelicarioError> {
|
||||||
let passphrase_nfc = passphrase.nfc().collect::<String>(); // normalize once
|
let passphrase_nfc = passphrase.nfc().collect::<String>(); // normalize once
|
||||||
|
|
||||||
let mut password = Zeroizing::new(
|
let mut password = Zeroizing::new(
|
||||||
@@ -461,13 +461,13 @@ Per-encryption layout (item, manifest, settings, each attachment):
|
|||||||
- `VERSION_BYTE = 0x02` (clean break — no v1 compat).
|
- `VERSION_BYTE = 0x02` (clean break — no v1 compat).
|
||||||
- XChaCha20-Poly1305 (already correct, audit confirmed-safe #1).
|
- XChaCha20-Poly1305 (already correct, audit confirmed-safe #1).
|
||||||
- Fresh `OsRng`-derived nonce per encryption.
|
- Fresh `OsRng`-derived nonce per encryption.
|
||||||
- Decrypt failure returns opaque `IdfotoError::Decrypt` regardless of which validation tripped (audit M4).
|
- Decrypt failure returns opaque `RelicarioError::Decrypt` regardless of which validation tripped (audit M4).
|
||||||
|
|
||||||
### RNG (audit H5, H6)
|
### RNG (audit H5, H6)
|
||||||
|
|
||||||
- `idfoto-wasm` uses `getrandom` (with `js` feature) for password generation, item IDs, attachment IDs. **No `Math.random()` anywhere.**
|
- `relicario-wasm` uses `getrandom` (with `js` feature) for password generation, item IDs, attachment IDs. **No `Math.random()` anywhere.**
|
||||||
- Modulo-bias eliminated via `rand::distributions::Uniform` for charset sampling — both CLI and WASM paths.
|
- Modulo-bias eliminated via `rand::distributions::Uniform` for charset sampling — both CLI and WASM paths.
|
||||||
- Single canonical `generate_password` and `generate_bip39` in `idfoto-core`, exposed to WASM and called directly by CLI.
|
- Single canonical `generate_password` and `generate_bip39` in `relicario-core`, exposed to WASM and called directly by CLI.
|
||||||
|
|
||||||
### ID format (audit M8)
|
### ID format (audit M8)
|
||||||
|
|
||||||
@@ -476,14 +476,14 @@ Per-encryption layout (item, manifest, settings, each attachment):
|
|||||||
|
|
||||||
### Per-vault crypto metadata
|
### Per-vault crypto metadata
|
||||||
|
|
||||||
`.idfoto/params.json`:
|
`.relicario/params.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"format_version": 2,
|
"format_version": 2,
|
||||||
"kdf": { "algorithm": "argon2id-v0x13", "m": 65536, "t": 3, "p": 4 },
|
"kdf": { "algorithm": "argon2id-v0x13", "m": 65536, "t": 3, "p": 4 },
|
||||||
"aead": "xchacha20poly1305",
|
"aead": "xchacha20poly1305",
|
||||||
"salt_path": ".idfoto/salt"
|
"salt_path": ".relicario/salt"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ Three version fields exist intentionally and evolve independently:
|
|||||||
|
|
||||||
| Field | Where | Bumps when |
|
| Field | Where | Bumps when |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `format_version` | `.idfoto/params.json` | Overall vault layout changes (file structure, KDF construction, anything cross-cutting) |
|
| `format_version` | `.relicario/params.json` | Overall vault layout changes (file structure, KDF construction, anything cross-cutting) |
|
||||||
| `schema_version` | inside `manifest.enc` | Manifest entry shape changes only (e.g., adding a new field to `ManifestEntry`) |
|
| `schema_version` | inside `manifest.enc` | Manifest entry shape changes only (e.g., adding a new field to `ManifestEntry`) |
|
||||||
| `VERSION_BYTE` | first byte of every AEAD blob | AEAD construction itself changes (cipher, nonce size, tag layout) |
|
| `VERSION_BYTE` | first byte of every AEAD blob | AEAD construction itself changes (cipher, nonce size, tag layout) |
|
||||||
|
|
||||||
@@ -505,7 +505,7 @@ All three set to `2` for the initial typed-item release. Future bumps are indepe
|
|||||||
|
|
||||||
- `setup.html` and `setup.js` **removed from `web_accessible_resources`** in both `extension/manifest.json` and `extension/manifest.firefox.json`.
|
- `setup.html` and `setup.js` **removed from `web_accessible_resources`** in both `extension/manifest.json` and `extension/manifest.firefox.json`.
|
||||||
- The popup opens setup via `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` — own-origin extension tabs work without WAR.
|
- The popup opens setup via `chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') })` — own-origin extension tabs work without WAR.
|
||||||
- WASM artifacts (`idfoto_wasm.js`, `idfoto_wasm_bg.wasm`) removed from WAR — service worker loads them via `import` from extension origin.
|
- WASM artifacts (`relicario_wasm.js`, `relicario_wasm_bg.wasm`) removed from WAR — service worker loads them via `import` from extension origin.
|
||||||
|
|
||||||
### Split message router (audit C1, C2, C4)
|
### Split message router (audit C1, C2, C4)
|
||||||
|
|
||||||
@@ -593,7 +593,7 @@ root.appendChild(promptDom);
|
|||||||
Strict rules for content-script DOM construction:
|
Strict rules for content-script DOM construction:
|
||||||
|
|
||||||
1. **No `innerHTML` anywhere in content scripts.** All construction via `document.createElement` + `.textContent =`.
|
1. **No `innerHTML` anywhere in content scripts.** All construction via `document.createElement` + `.textContent =`.
|
||||||
2. **Element IDs randomized per-prompt** (no stable `idfoto-save-btn` for page collisions). Use a per-prompt `Map<string, HTMLElement>` to wire up handlers.
|
2. **Element IDs randomized per-prompt** (no stable `relicario-save-btn` for page collisions). Use a per-prompt `Map<string, HTMLElement>` to wire up handlers.
|
||||||
3. **Page-derived values bounded** — username field from `findUsernameValue` capped at 256 chars, control characters stripped, then assigned only via `.textContent`.
|
3. **Page-derived values bounded** — username field from `findUsernameValue` capped at 256 chars, control characters stripped, then assigned only via `.textContent`.
|
||||||
4. **CSS scoped via Shadow DOM** — no leak to/from page CSS.
|
4. **CSS scoped via Shadow DOM** — no leak to/from page CSS.
|
||||||
|
|
||||||
@@ -657,7 +657,7 @@ These audit items are bundled into the Phase 0 remediation plan (not this Phase
|
|||||||
- M7: CLI stdout `Password: ********` by default + `--show` flag.
|
- M7: CLI stdout `Password: ********` by default + `--show` flag.
|
||||||
- M11: CLI ISO-8601 timestamp formatting.
|
- M11: CLI ISO-8601 timestamp formatting.
|
||||||
- L7: `cargo audit` / `cargo deny` CI configuration.
|
- L7: `cargo audit` / `cargo deny` CI configuration.
|
||||||
- L8: CLI vault-dir detection (refuse to operate outside an `.idfoto/`-marked directory).
|
- L8: CLI vault-dir detection (refuse to operate outside an `.relicario/`-marked directory).
|
||||||
|
|
||||||
## WASM API Surface
|
## WASM API Surface
|
||||||
|
|
||||||
@@ -720,38 +720,38 @@ New commands and renamed semantics:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Existing (semantics carry forward, terminology updated to "item")
|
# Existing (semantics carry forward, terminology updated to "item")
|
||||||
idfoto init
|
relicario init
|
||||||
idfoto unlock # unlocks for next command
|
relicario unlock # unlocks for next command
|
||||||
idfoto lock
|
relicario lock
|
||||||
idfoto sync # git pull --rebase + push, hardened
|
relicario sync # git pull --rebase + push, hardened
|
||||||
idfoto generate [--length N] [--bip39 [--words N]] [--symbols safe|extended]
|
relicario generate [--length N] [--bip39 [--words N]] [--symbols safe|extended]
|
||||||
idfoto device <add|list|revoke>
|
relicario device <add|list|revoke>
|
||||||
|
|
||||||
# Updated for typed items
|
# Updated for typed items
|
||||||
idfoto add <type> [--title T] [--group G] [--tags t1,t2] [--favorite]
|
relicario add <type> [--title T] [--group G] [--tags t1,t2] [--favorite]
|
||||||
[...type-specific fields, e.g., --username, --url, --password-prompt]
|
[...type-specific fields, e.g., --username, --url, --password-prompt]
|
||||||
idfoto get <id-or-title> # always concealed by default; --show to reveal
|
relicario get <id-or-title> # always concealed by default; --show to reveal
|
||||||
idfoto list [--type T] [--group G] [--tag T] [--trashed]
|
relicario list [--type T] [--group G] [--tag T] [--trashed]
|
||||||
idfoto edit <id-or-title> # interactive prompts for fields to update
|
relicario edit <id-or-title> # interactive prompts for fields to update
|
||||||
# (no $EDITOR with plaintext — temp-file leak risk)
|
# (no $EDITOR with plaintext — temp-file leak risk)
|
||||||
idfoto rm <id-or-title> # soft-delete (trash)
|
relicario rm <id-or-title> # soft-delete (trash)
|
||||||
idfoto restore <id-or-title> # restore from trash
|
relicario restore <id-or-title> # restore from trash
|
||||||
idfoto purge <id-or-title> # hard-delete (also purges attachments)
|
relicario purge <id-or-title> # hard-delete (also purges attachments)
|
||||||
idfoto trash empty # hard-delete all past retention
|
relicario trash empty # hard-delete all past retention
|
||||||
|
|
||||||
# New for attachments
|
# New for attachments
|
||||||
idfoto attach <id-or-title> <file> # adds file as attachment
|
relicario attach <id-or-title> <file> # adds file as attachment
|
||||||
idfoto attachments <id-or-title> # list attachments on item
|
relicario attachments <id-or-title> # list attachments on item
|
||||||
idfoto extract <id-or-title> <aid> [--out path] # decrypt + save to disk
|
relicario extract <id-or-title> <aid> [--out path] # decrypt + save to disk
|
||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
idfoto settings get [<key>]
|
relicario settings get [<key>]
|
||||||
idfoto settings set <key> <value> # e.g., trash_retention=days:60
|
relicario settings set <key> <value> # e.g., trash_retention=days:60
|
||||||
```
|
```
|
||||||
|
|
||||||
`idfoto get` shows password as `********` by default. `--show` is required to print plaintext. Clipboard auto-clear unconditional after 30s with `Zeroizing<String>` wrap (audit M6, M7).
|
`relicario get` shows password as `********` by default. `--show` is required to print plaintext. Clipboard auto-clear unconditional after 30s with `Zeroizing<String>` wrap (audit M6, M7).
|
||||||
|
|
||||||
`vault_dir()` detection: traverses up from CWD looking for `.idfoto/`. Refuses to operate without one (audit L8).
|
`vault_dir()` detection: traverses up from CWD looking for `.relicario/`. Refuses to operate without one (audit L8).
|
||||||
|
|
||||||
## Browser Extension UI Implications
|
## Browser Extension UI Implications
|
||||||
|
|
||||||
@@ -792,7 +792,7 @@ Setup wizard, capture flow, autofill icon, and unlock screen all continue to exi
|
|||||||
|
|
||||||
- **Service worker router**: mock `chrome.runtime.onMessage` and verify each message type is rejected when sent from the wrong sender (popup-only from content, content-callable from popup, anything from external).
|
- **Service worker router**: mock `chrome.runtime.onMessage` and verify each message type is rejected when sent from the wrong sender (popup-only from content, content-callable from popup, anything from external).
|
||||||
- **Origin-bound autofill**: mock `sender.tab.url` and verify cross-origin requests are rejected even when the content script asks nicely.
|
- **Origin-bound autofill**: mock `sender.tab.url` and verify cross-origin requests are rejected even when the content script asks nicely.
|
||||||
- **Closed Shadow DOM**: render the capture prompt, verify the page-side `document.querySelector('#idfoto-save-btn')` returns null.
|
- **Closed Shadow DOM**: render the capture prompt, verify the page-side `document.querySelector('#relicario-save-btn')` returns null.
|
||||||
- **Generator**: verify no `Math.random()` reachable from any extension entry point (lint rule + runtime probe).
|
- **Generator**: verify no `Math.random()` reachable from any extension entry point (lint rule + runtime probe).
|
||||||
|
|
||||||
### Manual / observational
|
### Manual / observational
|
||||||
@@ -826,7 +826,7 @@ Setup wizard, capture flow, autofill icon, and unlock screen all continue to exi
|
|||||||
| H6 | High | `rand::distributions::Uniform` in CLI generator |
|
| H6 | High | `rand::distributions::Uniform` in CLI generator |
|
||||||
| H7 | High | Bump `rpassword` to 7.x (Phase 0) |
|
| H7 | High | Bump `rpassword` to 7.x (Phase 0) |
|
||||||
| H8 | High | Documented in setup wizard + README; fine-grained PAT guidance |
|
| H8 | High | Documented in setup wizard + README; fine-grained PAT guidance |
|
||||||
| M4 | Medium | Opaque `IdfotoError::Decrypt` for all decrypt failures |
|
| M4 | Medium | Opaque `RelicarioError::Decrypt` for all decrypt failures |
|
||||||
| M5 | Medium | Popup captures `(tab.id, tab.url)` at open; verifies on `fill_credentials` |
|
| M5 | Medium | Popup captures `(tab.id, tab.url)` at open; verifies on `fill_credentials` |
|
||||||
| M8 | Medium | 16-char hex IDs |
|
| M8 | Medium | 16-char hex IDs |
|
||||||
| M9 | Medium | Item type discriminant validation in deserializer |
|
| M9 | Medium | Item type discriminant validation in deserializer |
|
||||||
@@ -839,20 +839,20 @@ Phase 0 implementation handles the remaining items (M3, M6, M7, M11, L7, L8) out
|
|||||||
New files (Rust):
|
New files (Rust):
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/idfoto-core/src/item.rs # Item, Section, Field, FieldKind, FieldValue
|
crates/relicario-core/src/item.rs # Item, Section, Field, FieldKind, FieldValue
|
||||||
crates/idfoto-core/src/item_types/mod.rs
|
crates/relicario-core/src/item_types/mod.rs
|
||||||
crates/idfoto-core/src/item_types/login.rs
|
crates/relicario-core/src/item_types/login.rs
|
||||||
crates/idfoto-core/src/item_types/secure_note.rs
|
crates/relicario-core/src/item_types/secure_note.rs
|
||||||
crates/idfoto-core/src/item_types/identity.rs
|
crates/relicario-core/src/item_types/identity.rs
|
||||||
crates/idfoto-core/src/item_types/card.rs
|
crates/relicario-core/src/item_types/card.rs
|
||||||
crates/idfoto-core/src/item_types/key.rs
|
crates/relicario-core/src/item_types/key.rs
|
||||||
crates/idfoto-core/src/item_types/document.rs
|
crates/relicario-core/src/item_types/document.rs
|
||||||
crates/idfoto-core/src/item_types/totp.rs
|
crates/relicario-core/src/item_types/totp.rs
|
||||||
crates/idfoto-core/src/manifest.rs # rewritten
|
crates/relicario-core/src/manifest.rs # rewritten
|
||||||
crates/idfoto-core/src/settings.rs
|
crates/relicario-core/src/settings.rs
|
||||||
crates/idfoto-core/src/generators.rs
|
crates/relicario-core/src/generators.rs
|
||||||
crates/idfoto-core/src/attachment.rs
|
crates/relicario-core/src/attachment.rs
|
||||||
crates/idfoto-wasm/src/session.rs
|
crates/relicario-wasm/src/session.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
New files (extension):
|
New files (extension):
|
||||||
@@ -875,15 +875,15 @@ extension/src/popup/components/history.ts
|
|||||||
Heavily modified (Rust):
|
Heavily modified (Rust):
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/idfoto-core/src/lib.rs # re-exports + module declarations
|
crates/relicario-core/src/lib.rs # re-exports + module declarations
|
||||||
crates/idfoto-core/src/crypto.rs # length-prefix KDF, Zeroize, NFC
|
crates/relicario-core/src/crypto.rs # length-prefix KDF, Zeroize, NFC
|
||||||
crates/idfoto-core/src/entry.rs # DELETED — replaced by item.rs
|
crates/relicario-core/src/entry.rs # DELETED — replaced by item.rs
|
||||||
crates/idfoto-core/src/error.rs # opaque Decrypt variant only
|
crates/relicario-core/src/error.rs # opaque Decrypt variant only
|
||||||
crates/idfoto-core/Cargo.toml # add zeroize, zxcvbn, bip39, unicode-normalization
|
crates/relicario-core/Cargo.toml # add zeroize, zxcvbn, bip39, unicode-normalization
|
||||||
crates/idfoto-wasm/src/lib.rs # session-handle API, getrandom
|
crates/relicario-wasm/src/lib.rs # session-handle API, getrandom
|
||||||
crates/idfoto-wasm/Cargo.toml # update deps
|
crates/relicario-wasm/Cargo.toml # update deps
|
||||||
crates/idfoto-cli/src/main.rs # rewritten command handlers
|
crates/relicario-cli/src/main.rs # rewritten command handlers
|
||||||
crates/idfoto-cli/Cargo.toml # rpassword = "7", clipboard hardening
|
crates/relicario-cli/Cargo.toml # rpassword = "7", clipboard hardening
|
||||||
```
|
```
|
||||||
|
|
||||||
Heavily modified (extension):
|
Heavily modified (extension):
|
||||||
@@ -916,5 +916,5 @@ Documentation:
|
|||||||
```
|
```
|
||||||
README.md # update for typed items, security warnings
|
README.md # update for typed items, security warnings
|
||||||
CLAUDE.md # reflect new module structure
|
CLAUDE.md # reflect new module structure
|
||||||
docs/superpowers/specs/2026-04-11-idfoto-design.md # amend KDF section per H1; note format v2
|
docs/superpowers/specs/2026-04-11-relicario-design.md # amend KDF section per H1; note format v2
|
||||||
```
|
```
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"icons": {
|
"icons": {
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "idfoto@adlee.work",
|
"id": "relicario@adlee.work",
|
||||||
"strict_min_version": "128.0"
|
"strict_min_version": "128.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -36,6 +36,6 @@
|
|||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||||
},
|
},
|
||||||
"web_accessible_resources": [{
|
"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"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "idfoto",
|
"name": "relicario",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Two-factor encrypted password manager",
|
"description": "Two-factor encrypted password manager",
|
||||||
"icons": {
|
"icons": {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||||
},
|
},
|
||||||
"web_accessible_resources": [{
|
"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>"]
|
"matches": ["<all_urls>"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "idfoto-extension",
|
"name": "relicario-extension",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
|
"build:all": "npm run build:wasm && npm run build && npm run build:firefox",
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"dev:firefox": "webpack --config webpack.firefox.config.js --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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.1.40",
|
"@types/chrome": "^0.1.40",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// credentials in the vault. Supports bar and toast prompt styles.
|
/// credentials in the vault. Supports bar and toast prompt styles.
|
||||||
|
|
||||||
import type { Request, Response } from '../shared/messages';
|
import type { Request, Response } from '../shared/messages';
|
||||||
import type { IdfotoSettings } from '../shared/types';
|
import type { RelicarioSettings } from '../shared/types';
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ async function onFormSubmit(pwField: HTMLInputElement): Promise<void> {
|
|||||||
|
|
||||||
// Fetch settings for prompt style
|
// Fetch settings for prompt style
|
||||||
const settingsResp = await sendMessage({ type: 'get_settings' });
|
const settingsResp = await sendMessage({ type: 'get_settings' });
|
||||||
const settings: IdfotoSettings = settingsResp.ok
|
const settings: RelicarioSettings = settingsResp.ok
|
||||||
? (settingsResp.data as { settings: IdfotoSettings }).settings
|
? (settingsResp.data as { settings: RelicarioSettings }).settings
|
||||||
: { captureEnabled: true, captureStyle: 'bar' };
|
: { captureEnabled: true, captureStyle: 'bar' };
|
||||||
|
|
||||||
showPrompt(settings.captureStyle, data.action, url, username, password, data.entryId);
|
showPrompt(settings.captureStyle, data.action, url, username, password, data.entryId);
|
||||||
@@ -99,7 +99,7 @@ async function onFormSubmit(pwField: HTMLInputElement): Promise<void> {
|
|||||||
// --- Prompt UI ---
|
// --- Prompt UI ---
|
||||||
|
|
||||||
function removeExistingPrompt(): void {
|
function removeExistingPrompt(): void {
|
||||||
const existing = document.getElementById('idfoto-capture-prompt');
|
const existing = document.getElementById('relicario-capture-prompt');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ function showPrompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.id = 'idfoto-capture-prompt';
|
container.id = 'relicario-capture-prompt';
|
||||||
|
|
||||||
// Common styles
|
// Common styles
|
||||||
const baseStyles = [
|
const baseStyles = [
|
||||||
@@ -173,17 +173,17 @@ function showPrompt(
|
|||||||
<span style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
<span style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">
|
||||||
${actionLabel} login for <strong style="color:#58a6ff">${escapeForHtml(hostname)}</strong>${escapeForHtml(displayUser)}?
|
${actionLabel} login for <strong style="color:#58a6ff">${escapeForHtml(hostname)}</strong>${escapeForHtml(displayUser)}?
|
||||||
</span>
|
</span>
|
||||||
<button id="idfoto-save-btn" style="
|
<button id="relicario-save-btn" style="
|
||||||
background:#1f6feb; color:#fff; border:none; padding:5px 14px;
|
background:#1f6feb; color:#fff; border:none; padding:5px 14px;
|
||||||
border-radius:3px; cursor:pointer; font-family:inherit; font-size:12px;
|
border-radius:3px; cursor:pointer; font-family:inherit; font-size:12px;
|
||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
">${actionLabel}</button>
|
">${actionLabel}</button>
|
||||||
<button id="idfoto-never-btn" style="
|
<button id="relicario-never-btn" style="
|
||||||
background:transparent; color:#8b949e; border:1px solid #30363d;
|
background:transparent; color:#8b949e; border:1px solid #30363d;
|
||||||
padding:5px 10px; border-radius:3px; cursor:pointer;
|
padding:5px 10px; border-radius:3px; cursor:pointer;
|
||||||
font-family:inherit; font-size:12px; white-space:nowrap;
|
font-family:inherit; font-size:12px; white-space:nowrap;
|
||||||
">Never</button>
|
">Never</button>
|
||||||
<button id="idfoto-close-btn" style="
|
<button id="relicario-close-btn" style="
|
||||||
background:transparent; color:#8b949e; border:none;
|
background:transparent; color:#8b949e; border:none;
|
||||||
cursor:pointer; font-size:16px; padding:2px 6px;
|
cursor:pointer; font-size:16px; padding:2px 6px;
|
||||||
font-family:inherit; line-height:1;
|
font-family:inherit; line-height:1;
|
||||||
@@ -212,7 +212,7 @@ function showPrompt(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
container.querySelector('#idfoto-save-btn')?.addEventListener('click', async () => {
|
container.querySelector('#relicario-save-btn')?.addEventListener('click', async () => {
|
||||||
clearAutoDismiss();
|
clearAutoDismiss();
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
@@ -246,22 +246,22 @@ function showPrompt(
|
|||||||
// Show confirmation
|
// Show confirmation
|
||||||
const span = container.querySelector('span');
|
const span = container.querySelector('span');
|
||||||
if (span) span.textContent = '\u2713 Saved';
|
if (span) span.textContent = '\u2713 Saved';
|
||||||
const saveBtn = container.querySelector('#idfoto-save-btn') as HTMLElement | null;
|
const saveBtn = container.querySelector('#relicario-save-btn') as HTMLElement | null;
|
||||||
const neverBtn = container.querySelector('#idfoto-never-btn') as HTMLElement | null;
|
const neverBtn = container.querySelector('#relicario-never-btn') as HTMLElement | null;
|
||||||
if (saveBtn) saveBtn.style.display = 'none';
|
if (saveBtn) saveBtn.style.display = 'none';
|
||||||
if (neverBtn) neverBtn.style.display = 'none';
|
if (neverBtn) neverBtn.style.display = 'none';
|
||||||
setTimeout(() => removeExistingPrompt(), 1500);
|
setTimeout(() => removeExistingPrompt(), 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Never button
|
// Never button
|
||||||
container.querySelector('#idfoto-never-btn')?.addEventListener('click', async () => {
|
container.querySelector('#relicario-never-btn')?.addEventListener('click', async () => {
|
||||||
clearAutoDismiss();
|
clearAutoDismiss();
|
||||||
await sendMessage({ type: 'blacklist_site', hostname });
|
await sendMessage({ type: 'blacklist_site', hostname });
|
||||||
removeExistingPrompt();
|
removeExistingPrompt();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close button
|
// Close button
|
||||||
container.querySelector('#idfoto-close-btn')?.addEventListener('click', () => {
|
container.querySelector('#relicario-close-btn')?.addEventListener('click', () => {
|
||||||
clearAutoDismiss();
|
clearAutoDismiss();
|
||||||
removeExistingPrompt();
|
removeExistingPrompt();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function injectFieldIcons(
|
|||||||
const icon = document.createElement('div');
|
const icon = document.createElement('div');
|
||||||
icon.textContent = 'id';
|
icon.textContent = 'id';
|
||||||
icon.setAttribute('role', 'button');
|
icon.setAttribute('role', 'button');
|
||||||
icon.setAttribute('aria-label', 'idfoto autofill');
|
icon.setAttribute('aria-label', 'relicario autofill');
|
||||||
|
|
||||||
Object.assign(icon.style, {
|
Object.assign(icon.style, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -98,10 +98,10 @@ function showPicker(
|
|||||||
candidates: Array<[string, ManifestEntry]>,
|
candidates: Array<[string, ManifestEntry]>,
|
||||||
): void {
|
): void {
|
||||||
// Remove any existing picker.
|
// 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');
|
const picker = document.createElement('div');
|
||||||
picker.className = 'idfoto-picker';
|
picker.className = 'relicario-picker';
|
||||||
Object.assign(picker.style, {
|
Object.assign(picker.style, {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '0',
|
right: '0',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// Settings view — capture toggle, prompt style, and blacklist management.
|
/// Settings view — capture toggle, prompt style, and blacklist management.
|
||||||
|
|
||||||
import { sendMessage, navigate, escapeHtml } from '../popup';
|
import { 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> {
|
export async function renderSettings(app: HTMLElement): Promise<void> {
|
||||||
app.innerHTML = '<div class="pad" style="text-align:center; padding-top:20px;"><span class="spinner"></span></div>';
|
app.innerHTML = '<div class="pad" style="text-align:center; padding-top:20px;"><span class="spinner"></span></div>';
|
||||||
@@ -12,8 +12,8 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
|||||||
sendMessage({ type: 'get_blacklist' }),
|
sendMessage({ type: 'get_blacklist' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const settings: IdfotoSettings = settingsResp.ok
|
const settings: RelicarioSettings = settingsResp.ok
|
||||||
? (settingsResp.data as { settings: IdfotoSettings }).settings
|
? (settingsResp.data as { settings: RelicarioSettings }).settings
|
||||||
: { captureEnabled: false, captureStyle: 'bar' };
|
: { captureEnabled: false, captureStyle: 'bar' };
|
||||||
|
|
||||||
const blacklist: string[] = blacklistResp.ok
|
const blacklist: string[] = blacklistResp.ok
|
||||||
@@ -24,7 +24,7 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
|||||||
? blacklist.map((h) => `
|
? blacklist.map((h) => `
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; padding:4px 0; border-bottom:1px solid #21262d;">
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:4px 0; border-bottom:1px solid #21262d;">
|
||||||
<span style="font-size:12px; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(h)}</span>
|
<span style="font-size:12px; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(h)}</span>
|
||||||
<button class="idfoto-remove-bl" data-hostname="${escapeHtml(h)}" style="
|
<button class="relicario-remove-bl" data-hostname="${escapeHtml(h)}" style="
|
||||||
background:transparent; color:#f85149; border:none; cursor:pointer;
|
background:transparent; color:#f85149; border:none; cursor:pointer;
|
||||||
font-size:11px; padding:2px 6px;
|
font-size:11px; padding:2px 6px;
|
||||||
">remove</button>
|
">remove</button>
|
||||||
@@ -86,7 +86,7 @@ export async function renderSettings(app: HTMLElement): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Blacklist remove buttons
|
// Blacklist remove buttons
|
||||||
document.querySelectorAll('.idfoto-remove-bl').forEach((btn) => {
|
document.querySelectorAll('.relicario-remove-bl').forEach((btn) => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const hostname = (btn as HTMLElement).dataset.hostname;
|
const hostname = (btn as HTMLElement).dataset.hostname;
|
||||||
if (hostname) {
|
if (hostname) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { escapeHtml } from '../popup';
|
|||||||
export function renderSetupWizard(app: HTMLElement): void {
|
export function renderSetupWizard(app: HTMLElement): void {
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad" style="padding-top:24px;text-align:center;">
|
<div class="pad" style="padding-top:24px;text-align:center;">
|
||||||
<img class="brand-logo" src="icons/idfoto-logo.svg" alt="">
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
<div class="brand" style="font-size:16px;margin-bottom:4px;">idfoto</div>
|
<div class="brand" style="font-size:16px;margin-bottom:4px;">relicario</div>
|
||||||
<p class="secondary" style="margin-bottom:20px;">two-factor vault</p>
|
<p class="secondary" style="margin-bottom:20px;">two-factor vault</p>
|
||||||
|
|
||||||
<p class="muted" style="margin-bottom:16px;font-size:11px;line-height:1.6;">
|
<p class="muted" style="margin-bottom:16px;font-size:11px;line-height:1.6;">
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export function renderUnlock(app: HTMLElement): void {
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad" style="text-align:center; padding-top:40px;">
|
<div class="pad" style="text-align:center; padding-top:40px;">
|
||||||
<img class="brand-logo" src="icons/idfoto-logo.svg" alt="">
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
<div class="brand">idfoto</div>
|
<div class="brand">relicario</div>
|
||||||
<p class="muted" style="margin:8px 0 24px;">two-factor vault</p>
|
<p class="muted" style="margin:8px 0 24px;">two-factor vault</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=360">
|
<meta name="viewport" content="width=360">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>idfoto</title>
|
<title>relicario</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* idfoto extension — terminal dark theme */
|
/* relicario extension — terminal dark theme */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/// 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
|
/// In Chrome this runs as a service worker (MV3). In Firefox this runs
|
||||||
/// as a persistent background script. WASM loading adapts automatically.
|
/// as a persistent background script. WASM loading adapts automatically.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
/// and routes all messages from the popup and content scripts.
|
/// and routes all messages from the popup and content scripts.
|
||||||
|
|
||||||
import type { Request, Response } from '../shared/messages';
|
import type { Request, Response } from '../shared/messages';
|
||||||
import type { Manifest, VaultConfig, SetupState, IdfotoSettings } from '../shared/types';
|
import type { Manifest, VaultConfig, SetupState, RelicarioSettings } from '../shared/types';
|
||||||
import { DEFAULT_SETTINGS } from '../shared/types';
|
import { DEFAULT_SETTINGS } from '../shared/types';
|
||||||
import type { GitHost } from './git-host';
|
import type { GitHost } from './git-host';
|
||||||
import { createGitHost } from './git-host';
|
import { createGitHost } from './git-host';
|
||||||
@@ -30,9 +30,9 @@ const totpSecretCache: Map<string, string> = new Map();
|
|||||||
// We detect the environment at runtime and use the appropriate loading strategy.
|
// We detect the environment at runtime and use the appropriate loading strategy.
|
||||||
|
|
||||||
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
// @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
|
// @ts-ignore TS2307
|
||||||
import * as wasmBindings from '../../wasm/idfoto_wasm.js';
|
import * as wasmBindings from '../../wasm/relicario_wasm.js';
|
||||||
|
|
||||||
type WasmModule = typeof wasmBindings;
|
type WasmModule = typeof wasmBindings;
|
||||||
let wasm: WasmModule | null = null;
|
let wasm: WasmModule | null = null;
|
||||||
@@ -47,12 +47,12 @@ async function initWasm(): Promise<WasmModule> {
|
|||||||
|
|
||||||
if (isServiceWorker) {
|
if (isServiceWorker) {
|
||||||
// Chrome: fetch WASM binary and instantiate synchronously
|
// 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();
|
const wasmBytes = await wasmResponse.arrayBuffer();
|
||||||
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
initSync({ module: new WebAssembly.Module(wasmBytes) });
|
||||||
} else {
|
} else {
|
||||||
// Firefox: background script — async init works
|
// Firefox: background script — async init works
|
||||||
const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm');
|
const wasmUrl = chrome.runtime.getURL('relicario_wasm_bg.wasm');
|
||||||
await initDefault(wasmUrl);
|
await initDefault(wasmUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ async function loadSetupState(): Promise<SetupState> {
|
|||||||
|
|
||||||
// --- Settings & blacklist helpers ---
|
// --- Settings & blacklist helpers ---
|
||||||
|
|
||||||
async function loadSettings(): Promise<IdfotoSettings> {
|
async function loadSettings(): Promise<RelicarioSettings> {
|
||||||
const result = await chrome.storage.local.get('idfotoSettings');
|
const result = await chrome.storage.local.get('relicarioSettings');
|
||||||
return (result.idfotoSettings as IdfotoSettings) ?? { ...DEFAULT_SETTINGS };
|
return (result.relicarioSettings as RelicarioSettings) ?? { ...DEFAULT_SETTINGS };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings(settings: IdfotoSettings): Promise<void> {
|
async function saveSettings(settings: RelicarioSettings): Promise<void> {
|
||||||
await chrome.storage.local.set({ idfotoSettings: settings });
|
await chrome.storage.local.set({ relicarioSettings: settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBlacklist(): Promise<string[]> {
|
async function loadBlacklist(): Promise<string[]> {
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export interface VaultMeta {
|
|||||||
|
|
||||||
/// Read the vault salt and KDF params from the git repo.
|
/// Read the vault salt and KDF params from the git repo.
|
||||||
export async function fetchVaultMeta(git: GitHost): Promise<VaultMeta> {
|
export async function fetchVaultMeta(git: GitHost): Promise<VaultMeta> {
|
||||||
const saltBytes = await git.readFile('.idfoto/salt');
|
const saltBytes = await git.readFile('.relicario/salt');
|
||||||
const paramsRaw = await git.readFile('.idfoto/params.json');
|
const paramsRaw = await git.readFile('.relicario/params.json');
|
||||||
const paramsJson = new TextDecoder().decode(paramsRaw);
|
const paramsJson = new TextDecoder().decode(paramsRaw);
|
||||||
return { salt: saltBytes, paramsJson };
|
return { salt: saltBytes, paramsJson };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/// Vault initialization wizard — 4-step flow for creating new idfoto vaults.
|
/// Vault initialization wizard — 4-step flow for creating new relicario vaults.
|
||||||
///
|
///
|
||||||
/// Step 1: Choose host type (Gitea / GitHub)
|
/// Step 1: Choose host type (Gitea / GitHub)
|
||||||
/// Step 2: Configure connection (URL, repo, token) + test
|
/// Step 2: Configure connection (URL, repo, token) + test
|
||||||
@@ -11,16 +11,16 @@ import type { VaultConfig } from '../shared/types';
|
|||||||
|
|
||||||
// --- WASM module (loaded dynamically) ---
|
// --- WASM module (loaded dynamically) ---
|
||||||
|
|
||||||
type WasmModule = typeof import('idfoto-wasm');
|
type WasmModule = typeof import('relicario-wasm');
|
||||||
let wasm: WasmModule | null = null;
|
let wasm: WasmModule | null = null;
|
||||||
|
|
||||||
async function loadWasm(): Promise<WasmModule> {
|
async function loadWasm(): Promise<WasmModule> {
|
||||||
if (wasm) return wasm;
|
if (wasm) return wasm;
|
||||||
const mod = await import(
|
const mod = await import(
|
||||||
// @ts-ignore TS2307 — resolved at runtime, not by TS/webpack
|
// @ts-ignore TS2307 — resolved at runtime, not by TS/webpack
|
||||||
/* webpackIgnore: true */ '../idfoto_wasm.js'
|
/* webpackIgnore: true */ '../relicario_wasm.js'
|
||||||
) as WasmModule & { default: (input?: string | URL) => Promise<void> };
|
) as WasmModule & { default: (input?: string | URL) => Promise<void> };
|
||||||
await mod.default('../idfoto_wasm_bg.wasm');
|
await mod.default('../relicario_wasm_bg.wasm');
|
||||||
wasm = mod;
|
wasm = mod;
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
@@ -109,8 +109,8 @@ function render(): void {
|
|||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad" style="padding-top:12px;">
|
<div class="pad" style="padding-top:12px;">
|
||||||
<img class="brand-logo" src="icons/idfoto-logo.svg" alt="" style="margin-bottom:12px;">
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="" style="margin-bottom:12px;">
|
||||||
<div class="brand" style="margin-bottom:4px;">idfoto vault setup</div>
|
<div class="brand" style="margin-bottom:4px;">relicario vault setup</div>
|
||||||
${progressHtml}
|
${progressHtml}
|
||||||
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
|
||||||
${stepHtml}
|
${stepHtml}
|
||||||
@@ -412,14 +412,14 @@ function attachStep3(): void {
|
|||||||
const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
|
const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
|
||||||
|
|
||||||
await host.writeFile(
|
await host.writeFile(
|
||||||
'.idfoto/salt',
|
'.relicario/salt',
|
||||||
salt,
|
salt,
|
||||||
'init: vault salt',
|
'init: vault salt',
|
||||||
);
|
);
|
||||||
|
|
||||||
const paramsBytes = new TextEncoder().encode(paramsJson);
|
const paramsBytes = new TextEncoder().encode(paramsJson);
|
||||||
await host.writeFile(
|
await host.writeFile(
|
||||||
'.idfoto/params.json',
|
'.relicario/params.json',
|
||||||
paramsBytes,
|
paramsBytes,
|
||||||
'init: KDF parameters',
|
'init: KDF parameters',
|
||||||
);
|
);
|
||||||
@@ -427,7 +427,7 @@ function attachStep3(): void {
|
|||||||
const devicesJson = '{"devices":[]}';
|
const devicesJson = '{"devices":[]}';
|
||||||
const devicesBytes = new TextEncoder().encode(devicesJson);
|
const devicesBytes = new TextEncoder().encode(devicesJson);
|
||||||
await host.writeFile(
|
await host.writeFile(
|
||||||
'.idfoto/devices.json',
|
'.relicario/devices.json',
|
||||||
devicesBytes,
|
devicesBytes,
|
||||||
'init: device registry',
|
'init: device registry',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export type Request =
|
|||||||
| { type: 'check_credential'; url: string; username: string; password: string }
|
| { type: 'check_credential'; url: string; username: string; password: string }
|
||||||
| { type: 'blacklist_site'; hostname: string }
|
| { type: 'blacklist_site'; hostname: string }
|
||||||
| { type: 'get_settings' }
|
| { type: 'get_settings' }
|
||||||
| { type: 'update_settings'; settings: Partial<import('./types').IdfotoSettings> }
|
| { type: 'update_settings'; settings: Partial<import('./types').RelicarioSettings> }
|
||||||
| { type: 'get_blacklist' }
|
| { type: 'get_blacklist' }
|
||||||
| { type: 'remove_blacklist'; hostname: string };
|
| { type: 'remove_blacklist'; hostname: string };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/// Full credential entry (matches Rust Entry struct in idfoto-core).
|
/// Full credential entry (matches Rust Entry struct in relicario-core).
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -42,12 +42,12 @@ export interface SetupState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// User-configurable credential capture settings.
|
/// User-configurable credential capture settings.
|
||||||
export interface IdfotoSettings {
|
export interface RelicarioSettings {
|
||||||
captureEnabled: boolean;
|
captureEnabled: boolean;
|
||||||
captureStyle: 'bar' | 'toast';
|
captureStyle: 'bar' | 'toast';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: IdfotoSettings = {
|
export const DEFAULT_SETTINGS: RelicarioSettings = {
|
||||||
captureEnabled: false,
|
captureEnabled: false,
|
||||||
captureStyle: 'bar',
|
captureStyle: 'bar',
|
||||||
};
|
};
|
||||||
|
|||||||
4
extension/src/wasm.d.ts
vendored
4
extension/src/wasm.d.ts
vendored
@@ -1,9 +1,9 @@
|
|||||||
/// Type declarations for the idfoto WASM module produced by wasm-pack.
|
/// Type declarations for the relicario WASM module produced by wasm-pack.
|
||||||
|
|
||||||
// Ambient module declarations for the WASM glue code.
|
// Ambient module declarations for the WASM glue code.
|
||||||
// The module specifier must exactly match what's used in import statements.
|
// The module specifier must exactly match what's used in import statements.
|
||||||
|
|
||||||
declare module 'idfoto-wasm' {
|
declare module 'relicario-wasm' {
|
||||||
export default function init(input?: string | URL): Promise<void>;
|
export default function init(input?: string | URL): Promise<void>;
|
||||||
export function derive_master_key(
|
export function derive_master_key(
|
||||||
passphrase: string,
|
passphrase: string,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"idfoto-wasm": ["./wasm/idfoto_wasm.js"]
|
"relicario-wasm": ["./wasm/relicario_wasm.js"]
|
||||||
},
|
},
|
||||||
"baseUrl": "."
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ module.exports = {
|
|||||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||||
{ from: 'setup.html', to: '.' },
|
{ from: 'setup.html', to: '.' },
|
||||||
{ from: 'icons', to: 'icons' },
|
{ from: 'icons', to: 'icons' },
|
||||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||||
{ from: 'wasm/idfoto_wasm.js', to: '.' },
|
{ from: 'wasm/relicario_wasm.js', to: '.' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ module.exports = {
|
|||||||
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
{ from: 'src/popup/styles.css', to: 'styles.css' },
|
||||||
{ from: 'setup.html', to: '.' },
|
{ from: 'setup.html', to: '.' },
|
||||||
{ from: 'icons', to: 'icons' },
|
{ from: 'icons', to: 'icons' },
|
||||||
{ from: 'wasm/idfoto_wasm_bg.wasm', to: '.' },
|
{ from: 'wasm/relicario_wasm_bg.wasm', to: '.' },
|
||||||
{ from: 'wasm/idfoto_wasm.js', to: '.' },
|
{ from: 'wasm/relicario_wasm.js', to: '.' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user