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:
@@ -1,15 +1,15 @@
|
||||
[package]
|
||||
name = "idfoto-cli"
|
||||
name = "relicario-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "CLI for idfoto password manager"
|
||||
description = "CLI for relicario password manager"
|
||||
|
||||
[[bin]]
|
||||
name = "idfoto"
|
||||
name = "relicario"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
rpassword = "5"
|
||||
@@ -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
|
||||
//! [`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.
|
||||
//!
|
||||
//! ## Vault layout on disk
|
||||
//!
|
||||
//! ```text
|
||||
//! <vault_dir>/
|
||||
//! .idfoto/
|
||||
//! .relicario/
|
||||
//! salt # 32-byte random salt for Argon2id KDF
|
||||
//! params.json # KDF tuning parameters (m, t, p)
|
||||
//! devices.json # registered device public keys
|
||||
@@ -23,10 +23,10 @@
|
||||
//!
|
||||
//! 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).
|
||||
//! 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)`.
|
||||
//! 6. Use the master key to decrypt the manifest and/or individual entries.
|
||||
//!
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use idfoto_core::{
|
||||
use relicario_core::{
|
||||
decrypt_entry, decrypt_manifest, encrypt_entry, encrypt_manifest, generate_entry_id,
|
||||
Entry, KdfParams, Manifest, ManifestEntry,
|
||||
};
|
||||
@@ -56,7 +56,7 @@ use std::process::Command;
|
||||
/// Top-level CLI argument parser.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "idfoto",
|
||||
name = "relicario",
|
||||
version,
|
||||
about = "Git-backed password manager with reference image authentication"
|
||||
)]
|
||||
@@ -68,7 +68,7 @@ struct Cli {
|
||||
/// All available CLI subcommands.
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Initialize a new idfoto vault in the current directory.
|
||||
/// Initialize a new relicario vault in the current directory.
|
||||
/// Creates the directory structure, generates a random image secret,
|
||||
/// embeds it in the carrier image, and sets up git.
|
||||
Init {
|
||||
@@ -132,7 +132,7 @@ enum DeviceCommands {
|
||||
|
||||
// ─── 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
|
||||
/// itself (in the user's config directory); only the public key is stored
|
||||
@@ -149,23 +149,23 @@ struct DeviceEntry {
|
||||
// ─── Helper functions ───────────────────────────────────────────────────────
|
||||
|
||||
/// Returns the vault root directory (the current working directory).
|
||||
/// The vault is always rooted at the directory where `idfoto` is invoked.
|
||||
/// The vault is always rooted at the directory where `relicario` is invoked.
|
||||
fn vault_dir() -> PathBuf {
|
||||
std::env::current_dir().expect("failed to get current directory")
|
||||
}
|
||||
|
||||
/// Returns the path to the `.idfoto/` configuration directory within the vault.
|
||||
fn idfoto_dir() -> PathBuf {
|
||||
vault_dir().join(".idfoto")
|
||||
/// Returns the path to the `.relicario/` configuration directory within the vault.
|
||||
fn relicario_dir() -> PathBuf {
|
||||
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
|
||||
/// not secret (stored in plaintext) -- its purpose is to prevent precomputed
|
||||
/// rainbow table attacks against the Argon2id KDF.
|
||||
fn read_salt() -> Result<[u8; 32]> {
|
||||
let data = fs::read(idfoto_dir().join("salt")).context("failed to read salt")?;
|
||||
let data = fs::read(relicario_dir().join("salt")).context("failed to read salt")?;
|
||||
let mut salt = [0u8; 32];
|
||||
if data.len() != 32 {
|
||||
bail!("invalid salt file: expected 32 bytes, got {}", data.len());
|
||||
@@ -174,9 +174,9 @@ fn read_salt() -> Result<[u8; 32]> {
|
||||
Ok(salt)
|
||||
}
|
||||
|
||||
/// Read the KDF parameters from `.idfoto/params.json`.
|
||||
/// Read the KDF parameters from `.relicario/params.json`.
|
||||
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")?;
|
||||
let params: KdfParams = serde_json::from_str(&data).context("failed to parse params.json")?;
|
||||
Ok(params)
|
||||
@@ -184,10 +184,10 @@ fn read_params() -> Result<KdfParams> {
|
||||
|
||||
/// 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.
|
||||
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));
|
||||
}
|
||||
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 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 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")?;
|
||||
|
||||
Ok(master_key)
|
||||
@@ -318,7 +318,7 @@ fn generate_password(length: usize) -> String {
|
||||
|
||||
// ─── Command implementations ────────────────────────────────────────────────
|
||||
|
||||
/// Initialize a new idfoto vault in the current directory.
|
||||
/// Initialize a new relicario vault in the current directory.
|
||||
///
|
||||
/// Full sequence:
|
||||
/// 1. Read the carrier JPEG provided by the user.
|
||||
@@ -328,7 +328,7 @@ fn generate_password(length: usize) -> String {
|
||||
/// 5. Prompt for a passphrase (minimum 8 characters, with confirmation).
|
||||
/// 6. Generate a random 32-byte 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.
|
||||
/// 10. Initialize git and create the first commit.
|
||||
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
|
||||
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
|
||||
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
|
||||
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")?;
|
||||
|
||||
// 8. Create directory structure
|
||||
let idfoto = idfoto_dir();
|
||||
fs::create_dir_all(&idfoto).context("failed to create .idfoto directory")?;
|
||||
let relicario = relicario_dir();
|
||||
fs::create_dir_all(&relicario).context("failed to create .relicario directory")?;
|
||||
fs::create_dir_all(vault_dir().join("entries")).context("failed to create entries directory")?;
|
||||
|
||||
// 9. Write config files
|
||||
fs::write(idfoto.join("salt"), &salt).context("failed to write salt")?;
|
||||
fs::write(relicario.join("salt"), &salt).context("failed to write salt")?;
|
||||
fs::write(
|
||||
idfoto.join("params.json"),
|
||||
relicario.join("params.json"),
|
||||
serde_json::to_string_pretty(¶ms)?,
|
||||
)
|
||||
.context("failed to write params.json")?;
|
||||
fs::write(idfoto.join("devices.json"), "[]").context("failed to write devices.json")?;
|
||||
fs::write(relicario.join("devices.json"), "[]").context("failed to write devices.json")?;
|
||||
|
||||
// 10. Encrypt empty manifest
|
||||
let manifest = Manifest::new();
|
||||
@@ -403,7 +403,7 @@ fn cmd_init(image: PathBuf, output: PathBuf) -> Result<()> {
|
||||
if !status.success() {
|
||||
bail!("git init failed");
|
||||
}
|
||||
git_commit("feat: initialize idfoto vault")?;
|
||||
git_commit("feat: initialize relicario vault")?;
|
||||
|
||||
// 13. Success
|
||||
eprintln!("Vault initialized successfully.");
|
||||
@@ -757,24 +757,24 @@ fn cmd_sync() -> Result<()> {
|
||||
|
||||
// ─── Device management ──────────────────────────────────────────────────────
|
||||
|
||||
/// Read the device registry from `.idfoto/devices.json`.
|
||||
/// Read the device registry from `.relicario/devices.json`.
|
||||
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 devices: Vec<DeviceEntry> = serde_json::from_str(&data).context("failed to parse devices.json")?;
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
/// Write the device registry to `.idfoto/devices.json`.
|
||||
/// Write the device registry to `.relicario/devices.json`.
|
||||
fn write_devices(devices: &[DeviceEntry]) -> Result<()> {
|
||||
let data = serde_json::to_string_pretty(devices)?;
|
||||
fs::write(idfoto_dir().join("devices.json"), data).context("failed to write devices.json")?;
|
||||
fs::write(relicario_dir().join("devices.json"), data).context("failed to write devices.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a new device by generating an ed25519 keypair.
|
||||
///
|
||||
/// The private key is saved to `~/.config/idfoto/<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
|
||||
/// 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)
|
||||
let config_dir = dirs::config_dir()
|
||||
.context("failed to find config directory")?
|
||||
.join("idfoto");
|
||||
.join("relicario");
|
||||
fs::create_dir_all(&config_dir).context("failed to create config directory")?;
|
||||
let key_path = config_dir.join(format!("{}.key", name));
|
||||
fs::write(&key_path, &private_key_hex).context("failed to write private key")?;
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "idfoto-core"
|
||||
name = "relicario-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Core library for idfoto password manager"
|
||||
description = "Core library for relicario password manager"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2"
|
||||
@@ -41,7 +41,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! 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 chacha20poly1305::{
|
||||
@@ -51,7 +51,7 @@ use chacha20poly1305::{
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
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.
|
||||
const VERSION_BYTE: u8 = 0x01;
|
||||
@@ -74,7 +74,7 @@ const HEADER_LEN: usize = 1 + NONCE_LEN; // version + nonce
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`IdfotoError::Encrypt`] if the underlying AEAD operation fails
|
||||
/// Returns [`RelicarioError::Encrypt`] if the underlying AEAD operation fails
|
||||
/// (extremely unlikely in practice).
|
||||
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
@@ -88,7 +88,7 @@ pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
|
||||
|
||||
let ciphertext = cipher
|
||||
.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
|
||||
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
|
||||
/// 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
|
||||
/// padding oracle / chosen-ciphertext attacks).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`IdfotoError::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::Format`] if the data is too short or has an unknown version byte.
|
||||
/// - [`RelicarioError::Decrypt`] if the AEAD tag verification fails (wrong key or
|
||||
/// tampered data).
|
||||
pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
// Minimum valid blob: 1 (version) + 24 (nonce) + 16 (tag) = 41 bytes.
|
||||
// A zero-length plaintext produces exactly 41 bytes of output.
|
||||
if data.len() < HEADER_LEN + TAG_LEN {
|
||||
return Err(IdfotoError::Format(
|
||||
return Err(RelicarioError::Format(
|
||||
"data too short to be valid ciphertext".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let version = data[0];
|
||||
if version != VERSION_BYTE {
|
||||
return Err(IdfotoError::Format(format!(
|
||||
return Err(RelicarioError::Format(format!(
|
||||
"unknown version byte: 0x{:02x}",
|
||||
version
|
||||
)));
|
||||
@@ -135,14 +135,14 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result<Vec<u8>> {
|
||||
let cipher = XChaCha20Poly1305::new(key.into());
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|_| IdfotoError::Decrypt)?;
|
||||
.map_err(|_| RelicarioError::Decrypt)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// lets tests use fast params (m=256, t=1, p=1) while production uses strong
|
||||
/// 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.
|
||||
/// - `image_secret`: the 32-byte secret extracted from the reference JPEG via
|
||||
/// [`crate::imgsecret::extract`].
|
||||
/// - `salt`: a 32-byte vault-specific salt (stored in `.idfoto/salt`).
|
||||
/// - `params`: the Argon2id tuning parameters (stored in `.idfoto/params.json`).
|
||||
/// - `salt`: a 32-byte vault-specific salt (stored in `.relicario/salt`).
|
||||
/// - `params`: the Argon2id tuning parameters (stored in `.relicario/params.json`).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
@@ -200,7 +200,7 @@ impl Default for KdfParams {
|
||||
///
|
||||
/// # 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).
|
||||
pub fn derive_master_key(
|
||||
passphrase: &[u8],
|
||||
@@ -214,7 +214,7 @@ pub fn derive_master_key(
|
||||
params.argon2_p,
|
||||
Some(32),
|
||||
)
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
|
||||
|
||||
@@ -229,7 +229,7 @@ pub fn derive_master_key(
|
||||
let mut output = [0u8; 32];
|
||||
argon2
|
||||
.hash_password_into(&password, salt, &mut output)
|
||||
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
|
||||
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ mod tests {
|
||||
#[test]
|
||||
fn encrypt_decrypt_round_trip() {
|
||||
let key = [0xABu8; 32];
|
||||
let plaintext = b"hello, idfoto!";
|
||||
let plaintext = b"hello, relicario!";
|
||||
|
||||
let ciphertext = encrypt(&key, plaintext).unwrap();
|
||||
let decrypted = decrypt(&key, &ciphertext).unwrap();
|
||||
@@ -307,7 +307,7 @@ mod tests {
|
||||
let result = decrypt(&wrong_key, &ciphertext);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), IdfotoError::Decrypt));
|
||||
assert!(matches!(result.unwrap_err(), RelicarioError::Decrypt));
|
||||
}
|
||||
|
||||
#[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
|
||||
//! 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
|
||||
//! bindings, mobile FFI) straightforward.
|
||||
|
||||
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:
|
||||
/// KDF -> encryption -> decryption -> format parsing -> entry lookup -> image
|
||||
/// steganography -> serialization -> device keys.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IdfotoError {
|
||||
pub enum RelicarioError {
|
||||
/// The Argon2id key derivation failed. This typically means invalid KDF
|
||||
/// parameters were supplied (e.g., memory cost below Argon2's minimum).
|
||||
#[error("key derivation failed: {0}")]
|
||||
@@ -83,4 +83,4 @@ pub enum IdfotoError {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
//!
|
||||
//! This is the novel component of idfoto. It hides a 32-byte secret inside a
|
||||
//! This is the novel component of relicario. It hides a 32-byte secret inside a
|
||||
//! JPEG image's luminance channel using Quantization Index Modulation (QIM) on
|
||||
//! mid-frequency DCT coefficients, with majority voting across multiple redundant
|
||||
//! copies for robustness.
|
||||
@@ -39,7 +39,7 @@
|
||||
//! - Mild cropping (up to ~10% from edges, within the 15% crumple zone)
|
||||
//! - 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::ImageReader;
|
||||
use image::{ImageEncoder, Rgb, RgbImage};
|
||||
@@ -179,10 +179,10 @@ struct EmbedRegion {
|
||||
fn extract_y_channel(jpeg_bytes: &[u8]) -> Result<YChannel> {
|
||||
let reader = ImageReader::new(Cursor::new(jpeg_bytes))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width() as usize, rgb.height() as usize);
|
||||
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>> {
|
||||
let reader = ImageReader::new(Cursor::new(original_jpeg))
|
||||
.with_guessed_format()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to read image: {e}")))?;
|
||||
let img = reader
|
||||
.decode()
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to decode image: {e}")))?;
|
||||
let rgb = img.to_rgb8();
|
||||
let (width, height) = (rgb.width(), rgb.height());
|
||||
|
||||
@@ -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);
|
||||
encoder
|
||||
.write_image(output.as_raw(), width, height, image::ExtendedColorType::Rgb8)
|
||||
.map_err(|e| IdfotoError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
.map_err(|e| RelicarioError::ImgSecret(format!("failed to encode JPEG: {e}")))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -597,14 +597,14 @@ fn reconstruct_jpeg(original_jpeg: &[u8], y_modified: &YChannel) -> Result<Vec<u
|
||||
///
|
||||
/// # 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.
|
||||
/// - [`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>> {
|
||||
let mut y = extract_y_channel(carrier_jpeg)?;
|
||||
|
||||
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_height: MIN_DIMENSION,
|
||||
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;
|
||||
|
||||
if total_blocks < BLOCKS_PER_COPY * MIN_COPIES {
|
||||
return Err(IdfotoError::ImageTooSmall {
|
||||
return Err(RelicarioError::ImageTooSmall {
|
||||
min_width: MIN_DIMENSION,
|
||||
min_height: MIN_DIMENSION,
|
||||
actual_width: y.width as u32,
|
||||
@@ -669,7 +669,7 @@ pub fn embed(carrier_jpeg: &[u8], secret: &[u8; 32]) -> Result<Vec<u8>> {
|
||||
///
|
||||
/// # 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).
|
||||
pub fn extract(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
extract_with_crop_recovery(jpeg_bytes)
|
||||
@@ -695,7 +695,7 @@ fn try_extract_with_layout(
|
||||
) -> Result<[u8; 32]> {
|
||||
let positions = compute_embed_positions(orig_w, orig_h);
|
||||
if positions.is_empty() {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
|
||||
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];
|
||||
for i in 0..SECRET_BITS {
|
||||
if votes_total[i] == 0 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
let ones = votes_one[i];
|
||||
let zeros = votes_total[i] - ones;
|
||||
let majority = ones.max(zeros);
|
||||
let confidence = majority as f64 / votes_total[i] as f64;
|
||||
if confidence < 0.60 {
|
||||
return Err(IdfotoError::ExtractionFailed);
|
||||
return Err(RelicarioError::ExtractionFailed);
|
||||
}
|
||||
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)?;
|
||||
|
||||
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)
|
||||
@@ -830,7 +830,7 @@ fn extract_with_crop_recovery(jpeg_bytes: &[u8]) -> Result<[u8; 32]> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(IdfotoError::ExtractionFailed)
|
||||
Err(RelicarioError::ExtractionFailed)
|
||||
}
|
||||
|
||||
// ─── 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
|
||||
//! access, no network I/O, and no git operations. All inputs arrive as byte slices
|
||||
@@ -10,7 +10,7 @@
|
||||
//!
|
||||
//! ## 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
|
||||
//! encryption. This is the low-level "encrypt bytes / decrypt bytes" layer.
|
||||
//! - [`entry`] -- The vault data model: [`Entry`] (full credential),
|
||||
@@ -33,7 +33,7 @@
|
||||
//! ```
|
||||
|
||||
pub mod error;
|
||||
pub use error::{IdfotoError, Result};
|
||||
pub use error::{RelicarioError, Result};
|
||||
|
||||
pub mod crypto;
|
||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
||||
@@ -28,9 +28,9 @@ use crate::error::Result;
|
||||
///
|
||||
/// # 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).
|
||||
/// - [`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>> {
|
||||
let json = serde_json::to_vec(entry)?;
|
||||
crypto::encrypt(master_key, &json)
|
||||
@@ -40,10 +40,10 @@ pub fn encrypt_entry(master_key: &[u8; 32], entry: &Entry) -> Result<Vec<u8>> {
|
||||
///
|
||||
/// # 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.
|
||||
/// - [`crate::IdfotoError::Format`] if the ciphertext blob has an invalid header.
|
||||
/// - [`crate::IdfotoError::Json`] if the decrypted JSON is malformed.
|
||||
/// - [`crate::RelicarioError::Format`] if the ciphertext blob has an invalid header.
|
||||
/// - [`crate::RelicarioError::Json`] if the decrypted JSON is malformed.
|
||||
pub fn decrypt_entry(master_key: &[u8; 32], data: &[u8]) -> Result<Entry> {
|
||||
let json = crypto::decrypt(master_key, data)?;
|
||||
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,
|
||||
generate_entry_id, Entry, KdfParams, Manifest, ManifestEntry,
|
||||
};
|
||||
@@ -38,10 +38,10 @@ fn full_vault_workflow() {
|
||||
// 2. Generate random image_secret and embed
|
||||
let mut image_secret = [0u8; 32];
|
||||
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
|
||||
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");
|
||||
|
||||
// 4. Derive master_key with fast params
|
||||
@@ -1,14 +1,14 @@
|
||||
[package]
|
||||
name = "idfoto-wasm"
|
||||
name = "relicario-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WASM bindings for idfoto password manager"
|
||||
description = "WASM bindings for relicario password manager"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
idfoto-core = { path = "../idfoto-core" }
|
||||
relicario-core = { path = "../relicario-core" }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
serde_json = "1"
|
||||
@@ -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
|
||||
//! JavaScript after loading the compiled `.wasm` module.
|
||||
//!
|
||||
@@ -21,10 +21,10 @@
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use idfoto_core::crypto::{self, KdfParams};
|
||||
use idfoto_core::entry::Entry;
|
||||
use idfoto_core::vault;
|
||||
use idfoto_core::imgsecret;
|
||||
use relicario_core::crypto::{self, KdfParams};
|
||||
use relicario_core::entry::Entry;
|
||||
use relicario_core::vault;
|
||||
use relicario_core::imgsecret;
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
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
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
|
||||
idfoto_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
relicario_core::imgsecret::embed(carrier_jpeg, &secret)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ pub fn encrypt_manifest(manifest_json: &str, key: &[u8]) -> Result<Vec<u8>, JsVa
|
||||
let key: &[u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| JsValue::from_str("key must be exactly 32 bytes"))?;
|
||||
let manifest: idfoto_core::entry::Manifest =
|
||||
let manifest: relicario_core::entry::Manifest =
|
||||
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()))
|
||||
}
|
||||
Reference in New Issue
Block a user