use crate::binary::{read_u24_be, write_u24_be};
use std::io::{self, Read, Write};
pub const MAX_IV: u32 = 0x00FF_FFFF;
pub const RESERVED_POLICY_IV: u32 = 0;
pub const DEFAULT_ROTATION_THRESHOLD: u32 = 0x0080_0000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CollectionItem {
pub iv: u32,
pub ciphertext_and_tag: Vec<u8>,
}
impl CollectionItem {
pub fn new(iv: u32, ciphertext_and_tag: Vec<u8>) -> Self {
Self {
iv,
ciphertext_and_tag,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(3 + self.ciphertext_and_tag.len());
buf.push(((self.iv >> 16) & 0xFF) as u8);
buf.push(((self.iv >> 8) & 0xFF) as u8);
buf.push((self.iv & 0xFF) as u8);
buf.extend_from_slice(&self.ciphertext_and_tag);
buf
}
pub fn to_nanotdf_payload_bytes(&self) -> Vec<u8> {
let payload_len = 3 + self.ciphertext_and_tag.len();
let mut buf = Vec::with_capacity(3 + payload_len);
buf.push(((payload_len >> 16) & 0xFF) as u8);
buf.push(((payload_len >> 8) & 0xFF) as u8);
buf.push((payload_len & 0xFF) as u8);
buf.push(((self.iv >> 16) & 0xFF) as u8);
buf.push(((self.iv >> 8) & 0xFF) as u8);
buf.push((self.iv & 0xFF) as u8);
buf.extend_from_slice(&self.ciphertext_and_tag);
buf
}
pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
if bytes.len() < 3 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Collection item too small: need at least 3 bytes for IV",
));
}
let iv = ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | (bytes[2] as u32);
if iv == RESERVED_POLICY_IV {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Collection item has reserved IV 0 (used for encrypted policy)",
));
}
if iv > MAX_IV {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Collection item IV {} exceeds MAX_IV {}", iv, MAX_IV),
));
}
let ciphertext_and_tag = bytes[3..].to_vec();
Ok(Self {
iv,
ciphertext_and_tag,
})
}
pub fn from_nanotdf_payload_bytes(bytes: &[u8]) -> io::Result<Self> {
if bytes.len() < 6 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"NanoTDF payload too small: need at least 6 bytes (3 length + 3 IV)",
));
}
let length = ((bytes[0] as usize) << 16) | ((bytes[1] as usize) << 8) | (bytes[2] as usize);
if bytes.len() < 3 + length {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"NanoTDF payload truncated: expected {} bytes, got {}",
3 + length,
bytes.len()
),
));
}
Self::from_bytes(&bytes[3..3 + length])
}
pub fn read_from<R: Read>(reader: &mut R, total_len: usize) -> io::Result<Self> {
if total_len < 3 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Collection item too small: need at least 3 bytes for IV",
));
}
let mut bytes = vec![0u8; total_len];
reader.read_exact(&mut bytes)?;
Self::from_bytes(&bytes)
}
pub fn read_nanotdf_payload_from<R: Read>(reader: &mut R) -> io::Result<Self> {
let length = read_u24_be(reader)? as usize;
if length < 3 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"NanoTDF payload length too small: need at least 3 bytes for IV",
));
}
let mut payload = vec![0u8; length];
reader.read_exact(&mut payload)?;
Self::from_bytes(&payload)
}
pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&[
((self.iv >> 16) & 0xFF) as u8,
((self.iv >> 8) & 0xFF) as u8,
(self.iv & 0xFF) as u8,
])?;
writer.write_all(&self.ciphertext_and_tag)
}
pub fn write_nanotdf_payload_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
let payload_len = (3 + self.ciphertext_and_tag.len()) as u32;
write_u24_be(writer, payload_len)?;
self.write_to(writer)
}
pub fn container_size(&self) -> usize {
3 + self.ciphertext_and_tag.len()
}
pub fn nanotdf_payload_size(&self) -> usize {
3 + 3 + self.ciphertext_and_tag.len()
}
pub fn iv_bytes(&self) -> [u8; 3] {
[
((self.iv >> 16) & 0xFF) as u8,
((self.iv >> 8) & 0xFF) as u8,
(self.iv & 0xFF) as u8,
]
}
pub fn to_gcm_nonce(&self) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[9] = ((self.iv >> 16) & 0xFF) as u8;
nonce[10] = ((self.iv >> 8) & 0xFF) as u8;
nonce[11] = (self.iv & 0xFF) as u8;
nonce
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_collection_item_container_roundtrip() {
let item = CollectionItem::new(1, vec![0x01, 0x02, 0x03, 0x04]);
let bytes = item.to_bytes();
assert_eq!(bytes.len(), 7);
let parsed = CollectionItem::from_bytes(&bytes).unwrap();
assert_eq!(parsed.iv, 1);
assert_eq!(parsed.ciphertext_and_tag, vec![0x01, 0x02, 0x03, 0x04]);
}
#[test]
fn test_collection_item_nanotdf_payload_roundtrip() {
let item = CollectionItem::new(0x123456, vec![0xAA, 0xBB, 0xCC]);
let bytes = item.to_nanotdf_payload_bytes();
assert_eq!(bytes.len(), 9);
let parsed = CollectionItem::from_nanotdf_payload_bytes(&bytes).unwrap();
assert_eq!(parsed.iv, 0x123456);
assert_eq!(parsed.ciphertext_and_tag, vec![0xAA, 0xBB, 0xCC]);
}
#[test]
fn test_iv_to_gcm_nonce() {
let item = CollectionItem::new(0x010203, vec![]);
let nonce = item.to_gcm_nonce();
assert_eq!(&nonce[0..9], &[0u8; 9]);
assert_eq!(&nonce[9..12], &[0x01, 0x02, 0x03]);
}
#[test]
fn test_reserved_iv_rejected() {
let bytes = [0x00, 0x00, 0x00, 0x01, 0x02]; let result = CollectionItem::from_bytes(&bytes);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("reserved"));
}
#[test]
fn test_max_iv() {
let item = CollectionItem::new(MAX_IV, vec![0x42]);
let bytes = item.to_bytes();
let parsed = CollectionItem::from_bytes(&bytes).unwrap();
assert_eq!(parsed.iv, MAX_IV);
}
#[test]
fn test_reader_writer() {
let item = CollectionItem::new(42, vec![0x11, 0x22, 0x33, 0x44, 0x55]);
let mut buf = Vec::new();
item.write_to(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf);
let parsed = CollectionItem::read_from(&mut cursor, buf.len()).unwrap();
assert_eq!(parsed, item);
let mut buf2 = Vec::new();
item.write_nanotdf_payload_to(&mut buf2).unwrap();
let mut cursor2 = Cursor::new(&buf2);
let parsed2 = CollectionItem::read_nanotdf_payload_from(&mut cursor2).unwrap();
assert_eq!(parsed2, item);
}
#[test]
fn test_constants() {
assert_eq!(MAX_IV, 0x00FF_FFFF);
assert_eq!(RESERVED_POLICY_IV, 0);
assert_eq!(DEFAULT_ROTATION_THRESHOLD, 0x0080_0000);
}
}