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", "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]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@@ -1397,6 +1340,63 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 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]] [[package]]
name = "rpassword" name = "rpassword"
version = "5.0.1" version = "5.0.1"

View File

@@ -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",
] ]

View File

@@ -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 idfoto 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"

View File

@@ -1,14 +1,14 @@
//! idfoto CLI -- the platform layer for the idfoto password manager. //! idfoto CLI -- the platform layer for the idfoto 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,
}; };
@@ -57,7 +57,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"
)] )]
@@ -133,7 +133,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
@@ -155,12 +155,12 @@ 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 idfoto_dir() -> PathBuf {
vault_dir().join(".idfoto") 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 /// 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
@@ -175,7 +175,7 @@ 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(idfoto_dir().join("params.json"))
.context("failed to read params.json")?; .context("failed to read params.json")?;
@@ -185,10 +185,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")?;
@@ -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 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, &params) let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
.context("failed to derive master key")?; .context("failed to derive master key")?;
Ok(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). /// 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<()> {
@@ -342,7 +342,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")?;
@@ -371,7 +371,7 @@ 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, &params) let master_key = relicario_core::derive_master_key(passphrase.as_bytes(), &image_secret, &salt, &params)
.context("failed to derive master key")?; .context("failed to derive master key")?;
// 8. Create directory structure // 8. Create directory structure
@@ -758,7 +758,7 @@ 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 = idfoto_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")?;
@@ -766,7 +766,7 @@ fn read_devices() -> Result<Vec<DeviceEntry>> {
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(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) // 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")?;

View File

@@ -1,5 +1,5 @@
[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 idfoto password manager"

View File

@@ -43,7 +43,7 @@ impl From<&AttachmentRef> for AttachmentSummary {
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::crypto::{decrypt, encrypt}; use crate::crypto::{decrypt, encrypt};
use crate::error::{IdfotoError, Result}; use crate::error::{RelicarioError, Result};
/// Encrypted attachment with the AID derived from plaintext content. /// Encrypted attachment with the AID derived from plaintext content.
#[derive(Debug)] #[derive(Debug)]
@@ -54,7 +54,7 @@ pub struct EncryptedAttachment {
/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`. /// 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. /// before any crypto work is done.
/// ///
/// ## Call-site adaptation /// ## Call-site adaptation
@@ -67,7 +67,7 @@ pub fn encrypt_attachment(
max_bytes: u64, max_bytes: u64,
) -> Result<EncryptedAttachment> { ) -> Result<EncryptedAttachment> {
if plaintext.len() as u64 > max_bytes { if plaintext.len() as u64 > max_bytes {
return Err(IdfotoError::AttachmentTooLarge { return Err(RelicarioError::AttachmentTooLarge {
size: plaintext.len() as u64, size: plaintext.len() as u64,
max: max_bytes, max: max_bytes,
}); });
@@ -118,7 +118,7 @@ mod crypto_tests {
fn oversize_attachment_rejected() { fn oversize_attachment_rejected() {
let plaintext = vec![0u8; 11_000_000]; let plaintext = vec![0u8; 11_000_000];
let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024); let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024);
assert!(matches!(err, Err(IdfotoError::AttachmentTooLarge { .. }))); assert!(matches!(err, Err(RelicarioError::AttachmentTooLarge { .. })));
} }
#[test] #[test]
@@ -127,7 +127,7 @@ mod crypto_tests {
let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap(); let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
let wrong = Zeroizing::new([0u8; 32]); let wrong = Zeroizing::new([0u8; 32]);
let err = decrypt_attachment(&enc.bytes, &wrong); 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 //! 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::{
@@ -53,7 +53,7 @@ use serde::{Deserialize, Serialize};
use unicode_normalization::UnicodeNormalization; use unicode_normalization::UnicodeNormalization;
use zeroize::Zeroizing; use zeroize::Zeroizing;
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.
pub const VERSION_BYTE: u8 = 0x02; pub const VERSION_BYTE: u8 = 0x02;
@@ -76,7 +76,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());
@@ -90,7 +90,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());
@@ -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 /// 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 found = data[0]; let found = data[0];
if found != VERSION_BYTE { if found != VERSION_BYTE {
return Err(IdfotoError::UnsupportedFormatVersion { return Err(RelicarioError::UnsupportedFormatVersion {
found, found,
expected: VERSION_BYTE, 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 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).
@@ -193,8 +193,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
/// ///
@@ -202,7 +202,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],
@@ -216,7 +216,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);
@@ -238,7 +238,7 @@ pub fn derive_master_key(
let mut output = Zeroizing::new([0u8; 32]); let mut output = Zeroizing::new([0u8; 32]);
argon2 argon2
.hash_password_into(password.as_slice(), salt, output.as_mut()) .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) Ok(output)
} }
@@ -316,7 +316,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]
@@ -410,7 +410,7 @@ mod tests {
let key = Zeroizing::new([0u8; 32]); let key = Zeroizing::new([0u8; 32]);
let err = decrypt(&*key, &blob).expect_err("v1 blob should fail decrypt"); let err = decrypt(&*key, &blob).expect_err("v1 blob should fail decrypt");
match err { match err {
IdfotoError::UnsupportedFormatVersion { found, expected } => { RelicarioError::UnsupportedFormatVersion { found, expected } => {
assert_eq!(found, 0x01); assert_eq!(found, 0x01);
assert_eq!(expected, 0x02); 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 //! 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 -> item lookup -> image /// KDF -> encryption -> decryption -> format parsing -> item lookup -> image
/// steganography -> serialization -> device keys. /// steganography -> serialization -> device keys.
#[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),
@@ -64,7 +64,7 @@ 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>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -72,13 +72,13 @@ mod tests {
#[test] #[test]
fn decrypt_error_message_is_opaque() { fn decrypt_error_message_is_opaque() {
let err = IdfotoError::Decrypt; let err = RelicarioError::Decrypt;
assert_eq!(format!("{}", err), "decryption failed"); assert_eq!(format!("{}", err), "decryption failed");
} }
#[test] #[test]
fn weak_passphrase_carries_score() { fn weak_passphrase_carries_score() {
let err = IdfotoError::WeakPassphrase { score: 1 }; let err = RelicarioError::WeakPassphrase { score: 1 };
let s = format!("{}", err); let s = format!("{}", err);
assert!(s.contains("passphrase")); assert!(s.contains("passphrase"));
assert!(s.contains("strength")); assert!(s.contains("strength"));
@@ -86,7 +86,7 @@ mod tests {
#[test] #[test]
fn attachment_too_large_reports_sizes() { 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); let s = format!("{}", err);
assert!(s.contains("11000000")); assert!(s.contains("11000000"));
assert!(s.contains("10485760")); assert!(s.contains("10485760"));
@@ -94,13 +94,13 @@ mod tests {
#[test] #[test]
fn item_not_found_carries_id() { 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")); assert!(format!("{}", err).contains("abc123"));
} }
#[test] #[test]
fn unsupported_format_version_reports_byte() { 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); let s = format!("{}", err);
assert!(s.contains("01") || s.contains("1")); assert!(s.contains("01") || s.contains("1"));
assert!(s.contains("02") || s.contains("2")); assert!(s.contains("02") || s.contains("2"));

View File

@@ -7,7 +7,7 @@ use rand::rngs::OsRng;
use rand::RngCore; use rand::RngCore;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::error::{IdfotoError, Result}; use crate::error::{RelicarioError, Result};
use crate::settings::{Capitalization, CharClasses, GeneratorRequest, SymbolCharset}; use crate::settings::{Capitalization, CharClasses, GeneratorRequest, SymbolCharset};
const SAFE_SYMBOLS: &[u8] = b"!@#$%^&*-_=+"; const SAFE_SYMBOLS: &[u8] = b"!@#$%^&*-_=+";
@@ -21,7 +21,7 @@ pub fn generate_password(req: &GeneratorRequest) -> Result<Zeroizing<String>> {
GeneratorRequest::Random { length, classes, symbol_charset } => { GeneratorRequest::Random { length, classes, symbol_charset } => {
random_password(*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(), "use generate_passphrase() for BIP39 requests".into(),
)), )),
} }
@@ -33,7 +33,7 @@ fn random_password(
symbol_charset: &SymbolCharset, symbol_charset: &SymbolCharset,
) -> Result<Zeroizing<String>> { ) -> Result<Zeroizing<String>> {
if length == 0 || length > 128 { 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(); let mut charset: Vec<u8> = Vec::new();
if classes.lower { charset.extend_from_slice(LOWER); } if classes.lower { charset.extend_from_slice(LOWER); }
@@ -45,7 +45,7 @@ fn random_password(
SymbolCharset::Extended => EXTENDED_SYMBOLS, SymbolCharset::Extended => EXTENDED_SYMBOLS,
SymbolCharset::Custom(s) => { SymbolCharset::Custom(s) => {
if !s.is_ascii() { if !s.is_ascii() {
return Err(IdfotoError::Format( return Err(RelicarioError::Format(
"SymbolCharset::Custom must be ASCII-only".into(), "SymbolCharset::Custom must be ASCII-only".into(),
)); ));
} }
@@ -55,7 +55,7 @@ fn random_password(
charset.extend_from_slice(symbols); charset.extend_from_slice(symbols);
} }
if charset.is_empty() { 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()); 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 } => { GeneratorRequest::Bip39 { word_count, separator, capitalization } => {
bip39_passphrase(*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(), "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>> { fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Result<Zeroizing<String>> {
if !matches!(word_count, 3..=12) { 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). // 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 // 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]); let mut entropy = Zeroizing::new([0u8; 16]);
OsRng.fill_bytes(entropy.as_mut_slice()); OsRng.fill_bytes(entropy.as_mut_slice());
let m = Mnemonic::from_entropy_in(Language::English, entropy.as_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| { let words: Vec<String> = m.words().take(word_count as usize).map(|w| {
match cap { match cap {
Capitalization::Lower => w.to_ascii_lowercase(), 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<()> { pub fn validate_passphrase_strength(p: &str) -> Result<()> {
let est = rate_passphrase(p); let est = rate_passphrase(p);
if est.score < 3 { if est.score < 3 {
return Err(IdfotoError::WeakPassphrase { score: est.score }); return Err(RelicarioError::WeakPassphrase { score: est.score });
} }
Ok(()) Ok(())
} }

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
//! # idfoto-core //! # relicario-core
//! //!
//! Platform-agnostic core library for the idfoto password manager. //! Platform-agnostic core library for the idfoto password manager.
//! //!
@@ -10,7 +10,7 @@
//! //!
//! ## Modules //! ## Modules
//! //!
//! - [`error`] — The unified error type ([`IdfotoError`]). //! - [`error`] — The unified error type ([`RelicarioError`]).
//! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and //! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and
//! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02. //! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02.
//! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`. //! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`.
@@ -38,7 +38,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, VERSION_BYTE}; 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. //! Attachment encrypt/decrypt + content-addressed AID + cap enforcement.
use idfoto_core::{ use relicario_core::{
AttachmentId, IdfotoError, AttachmentId, RelicarioError,
crypto::KdfParams, crypto::KdfParams,
decrypt_attachment, derive_master_key, encrypt_attachment, decrypt_attachment, derive_master_key, encrypt_attachment,
}; };
@@ -43,7 +43,7 @@ fn cap_enforcement_at_exact_max() {
// One byte over — should fail // One byte over — should fail
let err = encrypt_attachment(&plaintext, &key, 1023); let err = encrypt_attachment(&plaintext, &key, 1023);
match err { match err {
Err(IdfotoError::AttachmentTooLarge { size, max }) => { Err(RelicarioError::AttachmentTooLarge { size, max }) => {
assert_eq!(size, 1024); assert_eq!(size, 1024);
assert_eq!(max, 1023); assert_eq!(max, 1023);
} }

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
//! End-to-end integration tests for the typed-item core. //! End-to-end integration tests for the typed-item core.
use idfoto_core::{ use relicario_core::{
crypto::KdfParams, crypto::KdfParams,
derive_master_key, encrypt_item, decrypt_item, derive_master_key, encrypt_item, decrypt_item,
encrypt_manifest, decrypt_manifest, encrypt_manifest, decrypt_manifest,
encrypt_settings, decrypt_settings, encrypt_settings, decrypt_settings,
Field, FieldValue, Item, ItemCore, Manifest, Section, VaultSettings, 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 url::Url;
use zeroize::Zeroizing; use zeroize::Zeroizing;
@@ -97,7 +97,7 @@ fn field_history_persists_through_round_trip() {
#[test] #[test]
fn wrong_key_fails_with_opaque_decrypt() { fn wrong_key_fails_with_opaque_decrypt() {
use idfoto_core::IdfotoError; use relicario_core::RelicarioError;
let salt = [0u8; 32]; let salt = [0u8; 32];
let img = [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 item = Item::new("x".into(), ItemCore::SecureNote(SecureNoteCore::default()));
let blob = encrypt_item(&item, &right).unwrap(); let blob = encrypt_item(&item, &right).unwrap();
let err = decrypt_item(&blob, &wrong); 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] [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 idfoto password manager"
@@ -8,7 +8,7 @@ description = "WASM bindings for idfoto password manager"
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"

View File

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