refactor(core): rewrite IdfotoError variants for typed items
- Decrypt is now opaque (audit M4) - Add WeakPassphrase, AttachmentTooLarge, ItemNotFound, UnsupportedFormatVersion - Rename EntryNotFound → ItemNotFound across remaining call sites Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,50 +10,41 @@ use thiserror::Error;
|
|||||||
/// All errors that can originate from idfoto-core operations.
|
/// All errors that can originate from idfoto-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 -> entry 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 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}")]
|
#[error("key derivation failed: {0}")]
|
||||||
Kdf(String),
|
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}")]
|
#[error("encryption failed: {0}")]
|
||||||
Encrypt(String),
|
Encrypt(String),
|
||||||
|
|
||||||
/// Authenticated decryption failed. This means either the wrong master key
|
/// Authenticated decryption failed. Message intentionally opaque (audit M4).
|
||||||
/// was used (wrong passphrase or wrong reference image) or the ciphertext
|
#[error("decryption failed")]
|
||||||
/// 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")]
|
|
||||||
Decrypt,
|
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}")]
|
#[error("invalid vault format: {0}")]
|
||||||
Format(String),
|
Format(String),
|
||||||
|
|
||||||
/// A vault entry was looked up by ID but does not exist in the manifest.
|
#[error("unsupported vault format version: found 0x{found:02x}, expected 0x{expected:02x}")]
|
||||||
/// The string payload is the missing entry ID.
|
UnsupportedFormatVersion { found: u8, expected: u8 },
|
||||||
#[error("entry not found: {0}")]
|
|
||||||
EntryNotFound(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}")]
|
#[error("imgsecret: {0}")]
|
||||||
ImgSecret(String),
|
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}")]
|
#[error("image too small: need at least {min_width}x{min_height}, got {actual_width}x{actual_height}")]
|
||||||
ImageTooSmall {
|
ImageTooSmall {
|
||||||
min_width: u32,
|
min_width: u32,
|
||||||
@@ -62,25 +53,56 @@ pub enum IdfotoError {
|
|||||||
actual_height: 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")]
|
#[error("extraction failed: no valid secret found in image")]
|
||||||
ExtractionFailed,
|
ExtractionFailed,
|
||||||
|
|
||||||
/// JSON serialization or deserialization of an entry or manifest failed.
|
|
||||||
/// Wraps [`serde_json::Error`] transparently via `#[from]`.
|
|
||||||
#[error("json error: {0}")]
|
#[error("json error: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
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}")]
|
#[error("device key error: {0}")]
|
||||||
DeviceKey(String),
|
DeviceKey(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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, IdfotoError>;
|
||||||
|
|
||||||
|
#[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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user