chore: reconcile Plan 1A branch with idfoto→relicario rename

Renames crate directories and sweeps identifiers so Plan 1B can reference
the post-rename names throughout.

- git mv crates/idfoto-{core,cli,wasm} → crates/relicario-{core,cli,wasm}
- sed sweep: idfoto_core/idfoto-core/IdfotoError/IDFOTO_IMAGE/.idfoto/ etc.
- All 128 relicario-core tests pass post-sweep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-19 20:33:04 -04:00
parent 49b78203f8
commit 9c49e5e148
32 changed files with 172 additions and 172 deletions

114
Cargo.lock generated
View File

@@ -830,63 +830,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "idfoto-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
"clap",
"dirs",
"ed25519-dalek",
"hex",
"idfoto-core",
"rand",
"rpassword",
"serde",
"serde_json",
"zeroize",
]
[[package]]
name = "idfoto-core"
version = "0.1.0"
dependencies = [
"argon2",
"bip39",
"chacha20poly1305",
"chrono",
"ed25519-dalek",
"getrandom",
"hex",
"image",
"rand",
"serde",
"serde_json",
"sha2",
"thiserror 2.0.18",
"unicode-normalization",
"url",
"zeroize",
"zxcvbn",
]
[[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]]
name = "idna"
version = "1.1.0"
@@ -1397,6 +1340,63 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "relicario-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
"clap",
"dirs",
"ed25519-dalek",
"hex",
"rand",
"relicario-core",
"rpassword",
"serde",
"serde_json",
"zeroize",
]
[[package]]
name = "relicario-core"
version = "0.1.0"
dependencies = [
"argon2",
"bip39",
"chacha20poly1305",
"chrono",
"ed25519-dalek",
"getrandom",
"hex",
"image",
"rand",
"serde",
"serde_json",
"sha2",
"thiserror 2.0.18",
"unicode-normalization",
"url",
"zeroize",
"zxcvbn",
]
[[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]]
name = "rpassword"
version = "5.0.1"

View File

@@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = [
"crates/idfoto-core",
"crates/idfoto-cli",
"crates/idfoto-wasm",
"crates/relicario-core",
"crates/relicario-cli",
"crates/relicario-wasm",
]

View File

@@ -1,15 +1,15 @@
[package]
name = "idfoto-cli"
name = "relicario-cli"
version = "0.1.0"
edition = "2021"
description = "CLI for idfoto 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"

View File

@@ -1,14 +1,14 @@
//! idfoto CLI -- the platform layer for the idfoto 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,
};
@@ -57,7 +57,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"
)]
@@ -133,7 +133,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
@@ -155,12 +155,12 @@ 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.
/// Returns the path to the `.relicario/` configuration directory within the vault.
fn idfoto_dir() -> PathBuf {
vault_dir().join(".idfoto")
}
/// 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
@@ -175,7 +175,7 @@ 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"))
.context("failed to read params.json")?;
@@ -185,10 +185,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")?;
@@ -207,12 +207,12 @@ fn unlock(image_path: &PathBuf) -> Result<Zeroizing<[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, &params)
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
.context("failed to derive master key")?;
Ok(master_key)
@@ -329,7 +329,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<()> {
@@ -342,7 +342,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")?;
@@ -371,7 +371,7 @@ 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, &params)
let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
.context("failed to derive master key")?;
// 8. Create directory structure
@@ -758,7 +758,7 @@ 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 data = fs::read_to_string(&path).context("failed to read devices.json")?;
@@ -766,7 +766,7 @@ fn read_devices() -> Result<Vec<DeviceEntry>> {
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")?;
@@ -801,7 +801,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")?;

View File

@@ -1,5 +1,5 @@
[package]
name = "idfoto-core"
name = "relicario-core"
version = "0.1.0"
edition = "2021"
description = "Core library for idfoto password manager"

View File

@@ -43,7 +43,7 @@ impl From<&AttachmentRef> for AttachmentSummary {
use zeroize::Zeroizing;
use crate::crypto::{decrypt, encrypt};
use crate::error::{IdfotoError, Result};
use crate::error::{RelicarioError, Result};
/// Encrypted attachment with the AID derived from plaintext content.
#[derive(Debug)]
@@ -54,7 +54,7 @@ pub struct EncryptedAttachment {
/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`.
///
/// Returns [`IdfotoError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
/// Returns [`RelicarioError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
/// before any crypto work is done.
///
/// ## Call-site adaptation
@@ -67,7 +67,7 @@ pub fn encrypt_attachment(
max_bytes: u64,
) -> Result<EncryptedAttachment> {
if plaintext.len() as u64 > max_bytes {
return Err(IdfotoError::AttachmentTooLarge {
return Err(RelicarioError::AttachmentTooLarge {
size: plaintext.len() as u64,
max: max_bytes,
});
@@ -118,7 +118,7 @@ mod crypto_tests {
fn oversize_attachment_rejected() {
let plaintext = vec![0u8; 11_000_000];
let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024);
assert!(matches!(err, Err(IdfotoError::AttachmentTooLarge { .. })));
assert!(matches!(err, Err(RelicarioError::AttachmentTooLarge { .. })));
}
#[test]
@@ -127,7 +127,7 @@ mod crypto_tests {
let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
let wrong = Zeroizing::new([0u8; 32]);
let err = decrypt_attachment(&enc.bytes, &wrong);
assert!(matches!(err, Err(IdfotoError::Decrypt)));
assert!(matches!(err, Err(RelicarioError::Decrypt)));
}
}

View File

@@ -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::{
@@ -53,7 +53,7 @@ use serde::{Deserialize, Serialize};
use unicode_normalization::UnicodeNormalization;
use zeroize::Zeroizing;
use crate::error::{IdfotoError, Result};
use crate::error::{RelicarioError, Result};
/// Current binary format version. Increment this if the ciphertext layout changes.
pub const VERSION_BYTE: u8 = 0x02;
@@ -76,7 +76,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());
@@ -90,7 +90,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());
@@ -105,27 +105,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 found = data[0];
if found != VERSION_BYTE {
return Err(IdfotoError::UnsupportedFormatVersion {
return Err(RelicarioError::UnsupportedFormatVersion {
found,
expected: VERSION_BYTE,
});
@@ -137,14 +137,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).
@@ -193,8 +193,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
///
@@ -202,7 +202,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],
@@ -216,7 +216,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);
@@ -238,7 +238,7 @@ pub fn derive_master_key(
let mut output = Zeroizing::new([0u8; 32]);
argon2
.hash_password_into(password.as_slice(), salt, output.as_mut())
.map_err(|e| IdfotoError::Kdf(e.to_string()))?;
.map_err(|e| RelicarioError::Kdf(e.to_string()))?;
Ok(output)
}
@@ -316,7 +316,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]
@@ -410,7 +410,7 @@ mod tests {
let key = Zeroizing::new([0u8; 32]);
let err = decrypt(&*key, &blob).expect_err("v1 blob should fail decrypt");
match err {
IdfotoError::UnsupportedFormatVersion { found, expected } => {
RelicarioError::UnsupportedFormatVersion { found, expected } => {
assert_eq!(found, 0x01);
assert_eq!(expected, 0x02);
}

View File

@@ -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 -> item lookup -> image
/// steganography -> serialization -> device keys.
#[derive(Debug, Error)]
pub enum IdfotoError {
pub enum RelicarioError {
#[error("key derivation failed: {0}")]
Kdf(String),
@@ -64,7 +64,7 @@ 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>;
#[cfg(test)]
mod tests {
@@ -72,13 +72,13 @@ mod tests {
#[test]
fn decrypt_error_message_is_opaque() {
let err = IdfotoError::Decrypt;
let err = RelicarioError::Decrypt;
assert_eq!(format!("{}", err), "decryption failed");
}
#[test]
fn weak_passphrase_carries_score() {
let err = IdfotoError::WeakPassphrase { score: 1 };
let err = RelicarioError::WeakPassphrase { score: 1 };
let s = format!("{}", err);
assert!(s.contains("passphrase"));
assert!(s.contains("strength"));
@@ -86,7 +86,7 @@ mod tests {
#[test]
fn attachment_too_large_reports_sizes() {
let err = IdfotoError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 };
let err = RelicarioError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 };
let s = format!("{}", err);
assert!(s.contains("11000000"));
assert!(s.contains("10485760"));
@@ -94,13 +94,13 @@ mod tests {
#[test]
fn item_not_found_carries_id() {
let err = IdfotoError::ItemNotFound("abc123".to_string());
let err = RelicarioError::ItemNotFound("abc123".to_string());
assert!(format!("{}", err).contains("abc123"));
}
#[test]
fn unsupported_format_version_reports_byte() {
let err = IdfotoError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 };
let err = RelicarioError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 };
let s = format!("{}", err);
assert!(s.contains("01") || s.contains("1"));
assert!(s.contains("02") || s.contains("2"));

View File

@@ -7,7 +7,7 @@ use rand::rngs::OsRng;
use rand::RngCore;
use zeroize::Zeroizing;
use crate::error::{IdfotoError, Result};
use crate::error::{RelicarioError, Result};
use crate::settings::{Capitalization, CharClasses, GeneratorRequest, SymbolCharset};
const SAFE_SYMBOLS: &[u8] = b"!@#$%^&*-_=+";
@@ -21,7 +21,7 @@ pub fn generate_password(req: &GeneratorRequest) -> Result<Zeroizing<String>> {
GeneratorRequest::Random { length, classes, symbol_charset } => {
random_password(*length, classes, symbol_charset)
}
GeneratorRequest::Bip39 { .. } => Err(IdfotoError::Format(
GeneratorRequest::Bip39 { .. } => Err(RelicarioError::Format(
"use generate_passphrase() for BIP39 requests".into(),
)),
}
@@ -33,7 +33,7 @@ fn random_password(
symbol_charset: &SymbolCharset,
) -> Result<Zeroizing<String>> {
if length == 0 || length > 128 {
return Err(IdfotoError::Format("length must be 1..=128".into()));
return Err(RelicarioError::Format("length must be 1..=128".into()));
}
let mut charset: Vec<u8> = Vec::new();
if classes.lower { charset.extend_from_slice(LOWER); }
@@ -45,7 +45,7 @@ fn random_password(
SymbolCharset::Extended => EXTENDED_SYMBOLS,
SymbolCharset::Custom(s) => {
if !s.is_ascii() {
return Err(IdfotoError::Format(
return Err(RelicarioError::Format(
"SymbolCharset::Custom must be ASCII-only".into(),
));
}
@@ -55,7 +55,7 @@ fn random_password(
charset.extend_from_slice(symbols);
}
if charset.is_empty() {
return Err(IdfotoError::Format("at least one character class required".into()));
return Err(RelicarioError::Format("at least one character class required".into()));
}
let dist = Uniform::from(0..charset.len());
@@ -69,7 +69,7 @@ pub fn generate_passphrase(req: &GeneratorRequest) -> Result<Zeroizing<String>>
GeneratorRequest::Bip39 { word_count, separator, capitalization } => {
bip39_passphrase(*word_count, separator, *capitalization)
}
GeneratorRequest::Random { .. } => Err(IdfotoError::Format(
GeneratorRequest::Random { .. } => Err(RelicarioError::Format(
"use generate_password() for Random requests".into(),
)),
}
@@ -77,7 +77,7 @@ pub fn generate_passphrase(req: &GeneratorRequest) -> Result<Zeroizing<String>>
fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Result<Zeroizing<String>> {
if !matches!(word_count, 3..=12) {
return Err(IdfotoError::Format("word_count must be 3..=12".into()));
return Err(RelicarioError::Format("word_count must be 3..=12".into()));
}
// bip39 v2 requires entropy 128256 bits in multiples of 32 bits (4 bytes).
// We always generate 128 bits (16 bytes) → 12 words, then take the first
@@ -85,7 +85,7 @@ fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Re
let mut entropy = Zeroizing::new([0u8; 16]);
OsRng.fill_bytes(entropy.as_mut_slice());
let m = Mnemonic::from_entropy_in(Language::English, entropy.as_slice())
.map_err(|e| IdfotoError::Format(format!("bip39: {e}")))?;
.map_err(|e| RelicarioError::Format(format!("bip39: {e}")))?;
let words: Vec<String> = m.words().take(word_count as usize).map(|w| {
match cap {
Capitalization::Lower => w.to_ascii_lowercase(),
@@ -124,7 +124,7 @@ pub fn rate_passphrase(p: &str) -> StrengthEstimate {
pub fn validate_passphrase_strength(p: &str) -> Result<()> {
let est = rate_passphrase(p);
if est.score < 3 {
return Err(IdfotoError::WeakPassphrase { score: est.score });
return Err(RelicarioError::WeakPassphrase { score: est.score });
}
Ok(())
}

View File

@@ -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 ───────────────────────────────────────────────────────────────────

View File

@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use url::Url;
use zeroize::Zeroizing;
use crate::error::{IdfotoError, Result};
use crate::error::{RelicarioError, Result};
use crate::ids::{AttachmentId, FieldId};
use crate::item_types::TotpConfig;
use crate::time::MonthYear;
@@ -96,7 +96,7 @@ impl Field {
/// Verify kind/value discriminants match. Called after deserialization.
pub fn validate(&self) -> Result<()> {
if self.kind != self.value.kind() {
return Err(IdfotoError::Format(format!(
return Err(RelicarioError::Format(format!(
"field {}: kind {:?} does not match value discriminant {:?}",
self.id.as_str(),
self.kind,
@@ -182,7 +182,7 @@ impl Item {
for section in &mut self.sections {
if let Some(field) = section.fields.iter_mut().find(|f| &f.id == field_id) {
if field.value.kind() != new_value.kind() {
return Err(IdfotoError::Format(format!(
return Err(RelicarioError::Format(format!(
"field {}: cannot change kind from {:?} to {:?}",
field.id.as_str(), field.value.kind(), new_value.kind()
)));
@@ -199,7 +199,7 @@ impl Item {
return Ok(());
}
}
Err(IdfotoError::Format(format!("field {} not found", field_id.as_str())))
Err(RelicarioError::Format(format!("field {} not found", field_id.as_str())))
}
pub fn soft_delete(&mut self) {
@@ -247,7 +247,7 @@ fn serialize_history_value(value: &FieldValue) -> Result<Zeroizing<String>> {
let s = base32_encode(&cfg.secret);
Zeroizing::new(s)
}
_ => return Err(IdfotoError::Format("not a history-tracked kind".into())),
_ => return Err(RelicarioError::Format("not a history-tracked kind".into())),
};
Ok(s)
}

View File

@@ -1,4 +1,4 @@
//! # idfoto-core
//! # relicario-core
//!
//! Platform-agnostic core library for the idfoto password manager.
//!
@@ -10,7 +10,7 @@
//!
//! ## Modules
//!
//! - [`error`] — The unified error type ([`IdfotoError`]).
//! - [`error`] — The unified error type ([`RelicarioError`]).
//! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and
//! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02.
//! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`.
@@ -38,7 +38,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, VERSION_BYTE};

View File

@@ -1,7 +1,7 @@
//! Attachment encrypt/decrypt + content-addressed AID + cap enforcement.
use idfoto_core::{
AttachmentId, IdfotoError,
use relicario_core::{
AttachmentId, RelicarioError,
crypto::KdfParams,
decrypt_attachment, derive_master_key, encrypt_attachment,
};
@@ -43,7 +43,7 @@ fn cap_enforcement_at_exact_max() {
// One byte over — should fail
let err = encrypt_attachment(&plaintext, &key, 1023);
match err {
Err(IdfotoError::AttachmentTooLarge { size, max }) => {
Err(RelicarioError::AttachmentTooLarge { size, max }) => {
assert_eq!(size, 1024);
assert_eq!(max, 1023);
}

View File

@@ -1,12 +1,12 @@
//! Field history end-to-end: capture on update, prune by retention policy,
//! survive encrypt/decrypt round-trip.
use idfoto_core::{
use relicario_core::{
Field, FieldValue, HistoryRetention, Item, ItemCore, Section,
crypto::KdfParams,
derive_master_key, decrypt_item, encrypt_item,
};
use idfoto_core::item_types::LoginCore;
use relicario_core::item_types::LoginCore;
use zeroize::Zeroizing;
fn key() -> Zeroizing<[u8; 32]> {

View File

@@ -2,8 +2,8 @@
//! UnsupportedFormatVersion, length-prefix construction guarantees domain
//! separation.
use idfoto_core::{
IdfotoError,
use relicario_core::{
RelicarioError,
crypto::{KdfParams, VERSION_BYTE},
decrypt, derive_master_key, encrypt,
};
@@ -33,7 +33,7 @@ fn v1_blob_is_rejected_with_unsupported_format_version() {
// decrypt(key: &[u8; 32], data: &[u8])
let err = decrypt(&key, &blob);
match err {
Err(IdfotoError::UnsupportedFormatVersion { found, expected }) => {
Err(RelicarioError::UnsupportedFormatVersion { found, expected }) => {
assert_eq!(found, 0x01);
assert_eq!(expected, 0x02);
}

View File

@@ -11,7 +11,7 @@
//! counts before asserting proportions. The ±5pp tolerance is unchanged because
//! sample size is the same (~10k chars).
use idfoto_core::{
use relicario_core::{
Capitalization, CharClasses, GeneratorRequest, SymbolCharset,
generate_passphrase, generate_password, validate_passphrase_strength,
};

View File

@@ -1,13 +1,13 @@
//! End-to-end integration tests for the typed-item core.
use idfoto_core::{
use relicario_core::{
crypto::KdfParams,
derive_master_key, encrypt_item, decrypt_item,
encrypt_manifest, decrypt_manifest,
encrypt_settings, decrypt_settings,
Field, FieldValue, Item, ItemCore, Manifest, Section, VaultSettings,
};
use idfoto_core::item_types::{LoginCore, SecureNoteCore};
use relicario_core::item_types::{LoginCore, SecureNoteCore};
use url::Url;
use zeroize::Zeroizing;
@@ -97,7 +97,7 @@ fn field_history_persists_through_round_trip() {
#[test]
fn wrong_key_fails_with_opaque_decrypt() {
use idfoto_core::IdfotoError;
use relicario_core::RelicarioError;
let salt = [0u8; 32];
let img = [0u8; 32];
@@ -107,5 +107,5 @@ fn wrong_key_fails_with_opaque_decrypt() {
let item = Item::new("x".into(), ItemCore::SecureNote(SecureNoteCore::default()));
let blob = encrypt_item(&item, &right).unwrap();
let err = decrypt_item(&blob, &wrong);
assert!(matches!(err, Err(IdfotoError::Decrypt)));
assert!(matches!(err, Err(RelicarioError::Decrypt)));
}

View File

@@ -1,5 +1,5 @@
[package]
name = "idfoto-wasm"
name = "relicario-wasm"
version = "0.1.0"
edition = "2021"
description = "WASM bindings for idfoto password manager"
@@ -8,7 +8,7 @@ description = "WASM bindings for idfoto password manager"
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"

View File

@@ -1,6 +1,6 @@
//! WASM bindings for the idfoto 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()))
}