From 87ead533e5666508d34e401259c36b915dff6069 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 19 Apr 2026 10:48:49 -0400 Subject: [PATCH] feat(core): bump VERSION_BYTE to 0x02 with typed UnsupportedFormatVersion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean break from v1 — no migration. Decrypting a v1 blob now returns IdfotoError::UnsupportedFormatVersion { found: 0x01, expected: 0x02 }. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/idfoto-core/src/crypto.rs | 43 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/idfoto-core/src/crypto.rs b/crates/idfoto-core/src/crypto.rs index fea56ec..e2d7fa8 100644 --- a/crates/idfoto-core/src/crypto.rs +++ b/crates/idfoto-core/src/crypto.rs @@ -22,7 +22,7 @@ //! [version: 1 byte] [nonce: 24 bytes] [ciphertext + Poly1305 tag: variable] //! ``` //! -//! - **Version byte** (`0x01`): allows future format changes without ambiguity. +//! - **Version byte** (`0x02`): allows future format changes without ambiguity. //! Decryption rejects any version it does not recognize. //! - **Nonce** (24 bytes): randomly generated per encryption via [`OsRng`]. //! Stored alongside the ciphertext so the decryptor does not need out-of-band @@ -56,7 +56,7 @@ use zeroize::Zeroizing; use crate::error::{IdfotoError, Result}; /// Current binary format version. Increment this if the ciphertext layout changes. -const VERSION_BYTE: u8 = 0x01; +pub const VERSION_BYTE: u8 = 0x02; /// XChaCha20-Poly1305 nonce length: 192 bits = 24 bytes. const NONCE_LEN: usize = 24; @@ -123,12 +123,12 @@ pub fn decrypt(key: &[u8; 32], data: &[u8]) -> Result> { )); } - let version = data[0]; - if version != VERSION_BYTE { - return Err(IdfotoError::Format(format!( - "unknown version byte: 0x{:02x}", - version - ))); + let found = data[0]; + if found != VERSION_BYTE { + return Err(IdfotoError::UnsupportedFormatVersion { + found, + expected: VERSION_BYTE, + }); } let nonce = XNonce::from_slice(&data[1..1 + NONCE_LEN]); @@ -344,8 +344,8 @@ mod tests { let expected_len = 1 + 24 + plaintext.len() + 16; assert_eq!(ciphertext.len(), expected_len); - // Version byte must be 0x01 - assert_eq!(ciphertext[0], 0x01); + // Version byte must be 0x02 + assert_eq!(ciphertext[0], 0x02); } #[test] @@ -394,4 +394,27 @@ mod tests { let key: zeroize::Zeroizing<[u8; 32]> = derive_master_key(b"x", &img, &salt, ¶ms).unwrap(); assert_eq!(key.len(), 32); } + + #[test] + fn version_byte_is_0x02() { + assert_eq!(VERSION_BYTE, 0x02); + } + + #[test] + fn decrypt_rejects_v1_blob_with_typed_error() { + // Construct a v1-style blob: [0x01][24 nonce bytes][16 tag bytes]. + let mut blob = vec![0x01u8]; + blob.extend_from_slice(&[0u8; 24]); + blob.extend_from_slice(&[0u8; 16]); + + let key = Zeroizing::new([0u8; 32]); + let err = decrypt(&*key, &blob).expect_err("v1 blob should fail decrypt"); + match err { + IdfotoError::UnsupportedFormatVersion { found, expected } => { + assert_eq!(found, 0x01); + assert_eq!(expected, 0x02); + } + other => panic!("expected UnsupportedFormatVersion, got {:?}", other), + } + } }