opentdf_protocol/nanotdf/
collection.rs1use crate::binary::{read_u24_be, write_u24_be};
34use std::io::{self, Read, Write};
35
36pub const MAX_IV: u32 = 0x00FF_FFFF;
38
39pub const RESERVED_POLICY_IV: u32 = 0;
41
42pub const DEFAULT_ROTATION_THRESHOLD: u32 = 0x0080_0000;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct CollectionItem {
53 pub iv: u32,
56
57 pub ciphertext_and_tag: Vec<u8>,
61}
62
63impl CollectionItem {
64 pub fn new(iv: u32, ciphertext_and_tag: Vec<u8>) -> Self {
66 Self {
67 iv,
68 ciphertext_and_tag,
69 }
70 }
71
72 pub fn to_bytes(&self) -> Vec<u8> {
77 let mut buf = Vec::with_capacity(3 + self.ciphertext_and_tag.len());
78 buf.push(((self.iv >> 16) & 0xFF) as u8);
80 buf.push(((self.iv >> 8) & 0xFF) as u8);
81 buf.push((self.iv & 0xFF) as u8);
82 buf.extend_from_slice(&self.ciphertext_and_tag);
83 buf
84 }
85
86 pub fn to_nanotdf_payload_bytes(&self) -> Vec<u8> {
91 let payload_len = 3 + self.ciphertext_and_tag.len();
92 let mut buf = Vec::with_capacity(3 + payload_len);
93
94 buf.push(((payload_len >> 16) & 0xFF) as u8);
96 buf.push(((payload_len >> 8) & 0xFF) as u8);
97 buf.push((payload_len & 0xFF) as u8);
98
99 buf.push(((self.iv >> 16) & 0xFF) as u8);
101 buf.push(((self.iv >> 8) & 0xFF) as u8);
102 buf.push((self.iv & 0xFF) as u8);
103
104 buf.extend_from_slice(&self.ciphertext_and_tag);
105 buf
106 }
107
108 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
112 if bytes.len() < 3 {
113 return Err(io::Error::new(
114 io::ErrorKind::InvalidData,
115 "Collection item too small: need at least 3 bytes for IV",
116 ));
117 }
118
119 let iv = ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | (bytes[2] as u32);
121
122 if iv == RESERVED_POLICY_IV {
124 return Err(io::Error::new(
125 io::ErrorKind::InvalidData,
126 "Collection item has reserved IV 0 (used for encrypted policy)",
127 ));
128 }
129 if iv > MAX_IV {
130 return Err(io::Error::new(
131 io::ErrorKind::InvalidData,
132 format!("Collection item IV {} exceeds MAX_IV {}", iv, MAX_IV),
133 ));
134 }
135
136 let ciphertext_and_tag = bytes[3..].to_vec();
137
138 Ok(Self {
139 iv,
140 ciphertext_and_tag,
141 })
142 }
143
144 pub fn from_nanotdf_payload_bytes(bytes: &[u8]) -> io::Result<Self> {
148 if bytes.len() < 6 {
149 return Err(io::Error::new(
150 io::ErrorKind::InvalidData,
151 "NanoTDF payload too small: need at least 6 bytes (3 length + 3 IV)",
152 ));
153 }
154
155 let length = ((bytes[0] as usize) << 16) | ((bytes[1] as usize) << 8) | (bytes[2] as usize);
157
158 if bytes.len() < 3 + length {
160 return Err(io::Error::new(
161 io::ErrorKind::InvalidData,
162 format!(
163 "NanoTDF payload truncated: expected {} bytes, got {}",
164 3 + length,
165 bytes.len()
166 ),
167 ));
168 }
169
170 Self::from_bytes(&bytes[3..3 + length])
172 }
173
174 pub fn read_from<R: Read>(reader: &mut R, total_len: usize) -> io::Result<Self> {
178 if total_len < 3 {
179 return Err(io::Error::new(
180 io::ErrorKind::InvalidData,
181 "Collection item too small: need at least 3 bytes for IV",
182 ));
183 }
184
185 let mut bytes = vec![0u8; total_len];
186 reader.read_exact(&mut bytes)?;
187 Self::from_bytes(&bytes)
188 }
189
190 pub fn read_nanotdf_payload_from<R: Read>(reader: &mut R) -> io::Result<Self> {
194 let length = read_u24_be(reader)? as usize;
196
197 if length < 3 {
198 return Err(io::Error::new(
199 io::ErrorKind::InvalidData,
200 "NanoTDF payload length too small: need at least 3 bytes for IV",
201 ));
202 }
203
204 let mut payload = vec![0u8; length];
205 reader.read_exact(&mut payload)?;
206 Self::from_bytes(&payload)
207 }
208
209 pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
211 writer.write_all(&[
213 ((self.iv >> 16) & 0xFF) as u8,
214 ((self.iv >> 8) & 0xFF) as u8,
215 (self.iv & 0xFF) as u8,
216 ])?;
217 writer.write_all(&self.ciphertext_and_tag)
218 }
219
220 pub fn write_nanotdf_payload_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
222 let payload_len = (3 + self.ciphertext_and_tag.len()) as u32;
223 write_u24_be(writer, payload_len)?;
224 self.write_to(writer)
225 }
226
227 pub fn container_size(&self) -> usize {
229 3 + self.ciphertext_and_tag.len()
230 }
231
232 pub fn nanotdf_payload_size(&self) -> usize {
234 3 + 3 + self.ciphertext_and_tag.len()
235 }
236
237 pub fn iv_bytes(&self) -> [u8; 3] {
239 [
240 ((self.iv >> 16) & 0xFF) as u8,
241 ((self.iv >> 8) & 0xFF) as u8,
242 (self.iv & 0xFF) as u8,
243 ]
244 }
245
246 pub fn to_gcm_nonce(&self) -> [u8; 12] {
250 let mut nonce = [0u8; 12];
251 nonce[9] = ((self.iv >> 16) & 0xFF) as u8;
252 nonce[10] = ((self.iv >> 8) & 0xFF) as u8;
253 nonce[11] = (self.iv & 0xFF) as u8;
254 nonce
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use std::io::Cursor;
262
263 #[test]
264 fn test_collection_item_container_roundtrip() {
265 let item = CollectionItem::new(1, vec![0x01, 0x02, 0x03, 0x04]);
266 let bytes = item.to_bytes();
267
268 assert_eq!(bytes.len(), 7);
270
271 let parsed = CollectionItem::from_bytes(&bytes).unwrap();
272 assert_eq!(parsed.iv, 1);
273 assert_eq!(parsed.ciphertext_and_tag, vec![0x01, 0x02, 0x03, 0x04]);
274 }
275
276 #[test]
277 fn test_collection_item_nanotdf_payload_roundtrip() {
278 let item = CollectionItem::new(0x123456, vec![0xAA, 0xBB, 0xCC]);
279 let bytes = item.to_nanotdf_payload_bytes();
280
281 assert_eq!(bytes.len(), 9);
283
284 let parsed = CollectionItem::from_nanotdf_payload_bytes(&bytes).unwrap();
285 assert_eq!(parsed.iv, 0x123456);
286 assert_eq!(parsed.ciphertext_and_tag, vec![0xAA, 0xBB, 0xCC]);
287 }
288
289 #[test]
290 fn test_iv_to_gcm_nonce() {
291 let item = CollectionItem::new(0x010203, vec![]);
292 let nonce = item.to_gcm_nonce();
293
294 assert_eq!(&nonce[0..9], &[0u8; 9]);
296 assert_eq!(&nonce[9..12], &[0x01, 0x02, 0x03]);
298 }
299
300 #[test]
301 fn test_reserved_iv_rejected() {
302 let bytes = [0x00, 0x00, 0x00, 0x01, 0x02]; let result = CollectionItem::from_bytes(&bytes);
304 assert!(result.is_err());
305 assert!(result.unwrap_err().to_string().contains("reserved"));
306 }
307
308 #[test]
309 fn test_max_iv() {
310 let item = CollectionItem::new(MAX_IV, vec![0x42]);
311 let bytes = item.to_bytes();
312 let parsed = CollectionItem::from_bytes(&bytes).unwrap();
313 assert_eq!(parsed.iv, MAX_IV);
314 }
315
316 #[test]
317 fn test_reader_writer() {
318 let item = CollectionItem::new(42, vec![0x11, 0x22, 0x33, 0x44, 0x55]);
319
320 let mut buf = Vec::new();
322 item.write_to(&mut buf).unwrap();
323
324 let mut cursor = Cursor::new(&buf);
325 let parsed = CollectionItem::read_from(&mut cursor, buf.len()).unwrap();
326 assert_eq!(parsed, item);
327
328 let mut buf2 = Vec::new();
330 item.write_nanotdf_payload_to(&mut buf2).unwrap();
331
332 let mut cursor2 = Cursor::new(&buf2);
333 let parsed2 = CollectionItem::read_nanotdf_payload_from(&mut cursor2).unwrap();
334 assert_eq!(parsed2, item);
335 }
336
337 #[test]
338 fn test_constants() {
339 assert_eq!(MAX_IV, 0x00FF_FFFF);
340 assert_eq!(RESERVED_POLICY_IV, 0);
341 assert_eq!(DEFAULT_ROTATION_THRESHOLD, 0x0080_0000);
342 }
343}