diff --git a/crates/relicario-core/src/ids.rs b/crates/relicario-core/src/ids.rs index 58c403e..e6b2361 100644 --- a/crates/relicario-core/src/ids.rs +++ b/crates/relicario-core/src/ids.rs @@ -2,8 +2,9 @@ //! //! - `ItemId` and `FieldId` are random 16-char hex strings (64 bits of entropy) //! generated via `OsRng` (audit M8: bumped from the v1 8-char/32-bit format). -//! - `AttachmentId` is the first 16 hex chars of `sha256(plaintext)` — +//! - `AttachmentId` is the first 32 hex chars of `sha256(plaintext)` (128 bits) — //! content-addressed so identical plaintext blobs deduplicate naturally in git. +//! (audit I2/B4: bumped from 8-byte/64-bit format to prevent birthday collisions) use rand::rngs::OsRng; use rand::RngCore; @@ -29,6 +30,12 @@ impl ItemId { Self(hex::encode(bytes)) } pub fn as_str(&self) -> &str { &self.0 } + + /// Returns true if this ID is valid for filesystem paths. + /// Valid ItemIds are 16 lowercase hex chars. + pub fn is_valid(&self) -> bool { + self.0.len() == 16 && self.0.chars().all(|c| c.is_ascii_hexdigit()) + } } impl Default for ItemId { @@ -51,9 +58,15 @@ impl Default for FieldId { impl AttachmentId { pub fn from_plaintext(plaintext: &[u8]) -> Self { let digest = Sha256::digest(plaintext); - Self(hex::encode(&digest[..8])) + Self(hex::encode(&digest[..16])) // 16 bytes = 128 bits } pub fn as_str(&self) -> &str { &self.0 } + + /// Returns true if this ID is valid for filesystem paths. + /// Valid AttachmentIds are 32 lowercase hex chars. + pub fn is_valid(&self) -> bool { + self.0.len() == 32 && self.0.chars().all(|c| c.is_ascii_hexdigit()) + } } #[cfg(test)] @@ -106,12 +119,36 @@ mod tests { } #[test] - fn attachment_id_is_16_hex_chars() { + fn attachment_id_is_32_hex_chars() { let id = AttachmentId::from_plaintext(b"any bytes"); - assert_eq!(id.0.len(), 16); + assert_eq!(id.0.len(), 32); // 16 bytes = 32 hex chars = 128 bits assert!(id.0.chars().all(|c| c.is_ascii_hexdigit())); } + #[test] + fn item_id_is_valid_for_normal_ids() { + let id = ItemId::new(); + assert!(id.is_valid()); + } + + #[test] + fn item_id_is_invalid_for_traversal() { + let bad = ItemId("../../../etc".to_string()); + assert!(!bad.is_valid()); + } + + #[test] + fn attachment_id_is_valid_for_normal_ids() { + let id = AttachmentId::from_plaintext(b"test"); + assert!(id.is_valid()); + } + + #[test] + fn attachment_id_is_invalid_for_traversal() { + let bad = AttachmentId("../../passwd".to_string()); + assert!(!bad.is_valid()); + } + #[test] fn ids_serialize_as_bare_strings() { let item = ItemId("abcdef0123456789".to_string());