diff --git a/crates/idfoto-core/src/error.rs b/crates/idfoto-core/src/error.rs index 194c5f5..cd9be18 100644 --- a/crates/idfoto-core/src/error.rs +++ b/crates/idfoto-core/src/error.rs @@ -10,50 +10,41 @@ use thiserror::Error; /// All errors that can originate from idfoto-core operations. /// /// Variants are ordered roughly by the pipeline stage where they occur: -/// KDF -> encryption -> decryption -> format parsing -> entry lookup -> image +/// KDF -> encryption -> decryption -> format parsing -> item lookup -> image /// steganography -> serialization -> device keys. #[derive(Debug, Error)] pub enum IdfotoError { - /// 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}")] Kdf(String), - /// XChaCha20-Poly1305 encryption failed. In practice this is extremely rare - /// -- the only realistic cause is an internal library error, since the cipher - /// accepts arbitrary-length plaintext. #[error("encryption failed: {0}")] Encrypt(String), - /// Authenticated decryption failed. This means either the wrong master key - /// was used (wrong passphrase or wrong reference image) or the ciphertext - /// was tampered with / corrupted in transit or at rest. The error message is - /// intentionally vague to avoid leaking information about which factor was - /// wrong (passphrase vs. image). - #[error("decryption failed: wrong key or corrupted data")] + /// Authenticated decryption failed. Message intentionally opaque (audit M4). + #[error("decryption failed")] Decrypt, - /// The binary ciphertext blob does not match the expected format (e.g., - /// too short to contain the version byte + nonce + tag, or an unrecognized - /// version byte). This usually indicates file corruption or a version - /// mismatch between the writer and reader. #[error("invalid vault format: {0}")] Format(String), - /// A vault entry was looked up by ID but does not exist in the manifest. - /// The string payload is the missing entry ID. - #[error("entry not found: {0}")] - EntryNotFound(String), + #[error("unsupported vault format version: found 0x{found:02x}, expected 0x{expected:02x}")] + UnsupportedFormatVersion { found: u8, expected: u8 }, + + /// An item was looked up by ID but does not exist in the manifest. + #[error("item not found: {0}")] + ItemNotFound(String), + + /// A passphrase failed the strength gate at vault creation (audit H3). + #[error("passphrase strength insufficient (score {score}/4)")] + WeakPassphrase { score: u8 }, + + /// An attachment exceeded the per-attachment cap from VaultSettings. + #[error("attachment too large: {size} bytes > {max} bytes max")] + AttachmentTooLarge { size: u64, max: u64 }, - /// A general error from the image steganography subsystem (imgsecret). - /// Covers issues like failing to decode the carrier JPEG or failing to - /// encode the output JPEG after modification. #[error("imgsecret: {0}")] ImgSecret(String), - /// The carrier image is too small to hold the embedded secret with - /// sufficient redundancy. The embed region (central 70% of the image) - /// must contain at least `BLOCKS_PER_COPY * MIN_COPIES` 8x8 blocks. #[error("image too small: need at least {min_width}x{min_height}, got {actual_width}x{actual_height}")] ImageTooSmall { min_width: u32, @@ -62,25 +53,56 @@ pub enum IdfotoError { actual_height: u32, }, - /// Secret extraction from a JPEG failed. This can mean: - /// - The image never had a secret embedded in it. - /// - The image was recompressed below Q85, destroying the QIM watermarks. - /// - The image was cropped beyond the 15% crumple zone. - /// - Majority-vote confidence fell below the 60% threshold on one or more bits. #[error("extraction failed: no valid secret found in image")] ExtractionFailed, - /// JSON serialization or deserialization of an entry or manifest failed. - /// Wraps [`serde_json::Error`] transparently via `#[from]`. #[error("json error: {0}")] Json(#[from] serde_json::Error), - /// An error related to device ed25519 key operations. Device keys are - /// separate from the vault KDF -- revoking a device does not require - /// rotating the passphrase or reference image. #[error("device key error: {0}")] DeviceKey(String), } /// Crate-wide result alias, reducing boilerplate in function signatures. pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decrypt_error_message_is_opaque() { + let err = IdfotoError::Decrypt; + assert_eq!(format!("{}", err), "decryption failed"); + } + + #[test] + fn weak_passphrase_carries_score() { + let err = IdfotoError::WeakPassphrase { score: 1 }; + let s = format!("{}", err); + assert!(s.contains("passphrase")); + assert!(s.contains("strength")); + } + + #[test] + fn attachment_too_large_reports_sizes() { + let err = IdfotoError::AttachmentTooLarge { size: 11_000_000, max: 10_485_760 }; + let s = format!("{}", err); + assert!(s.contains("11000000")); + assert!(s.contains("10485760")); + } + + #[test] + fn item_not_found_carries_id() { + let err = IdfotoError::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 s = format!("{}", err); + assert!(s.contains("01") || s.contains("1")); + assert!(s.contains("02") || s.contains("2")); + } +}