feat(core): bump VERSION_BYTE to 0x02 with typed UnsupportedFormatVersion

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) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-19 10:48:49 -04:00
parent 2ea7658036
commit 87ead533e5

View File

@@ -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<Vec<u8>> {
));
}
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, &params).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),
}
}
}