//! Unified error type for the Relicario core crate. //! //! Every fallible function in this crate returns [`Result`], which is an alias //! for `std::result::Result`. 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 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 RelicarioError { /// The Argon2id key derivation failed. This typically means invalid KDF /// parameters were supplied (e.g., memory cost below Argon2's minimum). #[error("key derivation failed: {0}")] 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. 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), #[error("unsupported vault format version: found 0x{found:02x}, expected 0x{expected:02x}")] UnsupportedFormatVersion { found: u8, expected: u8 }, /// Backup file's first 4 bytes don't match the "RBAK" magic. #[error("not a Relicario backup file")] BackupBadMagic, /// Backup format version is newer than this binary supports. #[error("backup created by a newer Relicario; upgrade required")] BackupUnsupportedVersion { found: u8, expected: u8 }, /// Backup envelope schema version doesn't match. #[error("backup envelope schema v{found}; this Relicario reads v{expected}")] BackupSchemaMismatch { found: u32, expected: u32 }, /// An error during backup restore (e.g., tar safety validation failure). #[error("backup restore: {0}")] BackupRestore(String), /// CSV header doesn't match the LastPass column layout. #[error("unrecognized CSV header — expected LastPass export format ({0})")] ImportCsvHeader(String), /// CSV body could not be parsed (mismatched quoting, encoding, etc.). /// Per-row record errors that the importer recovers from become /// `ImportWarning` entries — this variant is reserved for failures /// that abort the whole import. #[error("CSV parse failed: {0}")] ImportCsvFormat(String), /// 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, min_height: u32, actual_width: u32, 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), /// HOTP requires incrementing and persisting the counter after each use. /// Without vault-save machinery in compute_totp_code, HOTP would desync /// immediately. Use TOTP instead. #[error("HOTP is not supported: counter persistence requires vault save after each use")] HotpNotSupported, } /// 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 = RelicarioError::Decrypt; assert_eq!(format!("{}", err), "decryption failed"); } #[test] fn weak_passphrase_carries_score() { let err = RelicarioError::WeakPassphrase { score: 1 }; let s = format!("{}", err); assert!(s.contains("passphrase")); assert!(s.contains("strength")); } #[test] fn attachment_too_large_reports_sizes() { let err = RelicarioError::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 = RelicarioError::ItemNotFound("abc123".to_string()); assert!(format!("{}", err).contains("abc123")); } #[test] fn unsupported_format_version_reports_byte() { 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")); } #[test] fn backup_errors_carry_useful_messages() { let bad = RelicarioError::BackupBadMagic; assert!(format!("{}", bad).contains("not a Relicario backup file")); let ver = RelicarioError::BackupUnsupportedVersion { found: 0x02, expected: 0x01 }; let s = format!("{}", ver); assert!(s.contains("newer")); let schema = RelicarioError::BackupSchemaMismatch { found: 2, expected: 1 }; let s = format!("{}", schema); assert!(s.contains("v2") && s.contains("v1")); } #[test] fn import_errors_carry_useful_messages() { let h = RelicarioError::ImportCsvHeader("missing 'name' column".into()); assert!(format!("{}", h).contains("LastPass")); assert!(format!("{}", h).contains("missing 'name'")); let f = RelicarioError::ImportCsvFormat("unterminated quote at line 12".into()); assert!(format!("{}", f).contains("CSV parse failed")); assert!(format!("{}", f).contains("unterminated quote")); } }