1use alloc::format;
13use alloc::vec::Vec;
14
15pub const BUILTIN_CRYPTO_PLUGIN: &str = "DDS:Crypto:AES_GCM_GMAC";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[repr(u32)]
29pub enum CryptoTransformKind {
30 None = 0,
32 Aes128Gmac = 1,
34 Aes128Gcm = 2,
36 Aes256Gmac = 3,
38 Aes256Gcm = 4,
40}
41
42impl CryptoTransformKind {
43 #[must_use]
45 pub const fn to_be_bytes(self) -> [u8; 4] {
46 (self as u32).to_be_bytes()
47 }
48
49 pub fn from_be_bytes(bytes: [u8; 4]) -> Result<Self, &'static str> {
54 match u32::from_be_bytes(bytes) {
55 0 => Ok(Self::None),
56 1 => Ok(Self::Aes128Gmac),
57 2 => Ok(Self::Aes128Gcm),
58 3 => Ok(Self::Aes256Gmac),
59 4 => Ok(Self::Aes256Gcm),
60 _ => Err("unknown CryptoTransformKind"),
61 }
62 }
63
64 #[must_use]
66 pub const fn encrypts(self) -> bool {
67 matches!(self, Self::Aes128Gcm | Self::Aes256Gcm)
68 }
69
70 #[must_use]
73 pub const fn tag_size(self) -> usize {
74 match self {
75 Self::None => 0,
76 _ => 16,
77 }
78 }
79
80 #[must_use]
82 pub const fn key_size(self) -> usize {
83 match self {
84 Self::None => 0,
85 Self::Aes128Gmac | Self::Aes128Gcm => 16,
86 Self::Aes256Gmac | Self::Aes256Gcm => 32,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct CryptoTransformIdentifier {
100 pub kind: CryptoTransformKind,
102 pub key_id: [u8; 4],
104}
105
106impl CryptoTransformIdentifier {
107 #[must_use]
109 pub fn new(kind: CryptoTransformKind, key_id: [u8; 4]) -> Self {
110 Self { kind, key_id }
111 }
112
113 #[must_use]
115 pub fn to_bytes(&self) -> [u8; 8] {
116 let mut out = [0u8; 8];
117 out[0..4].copy_from_slice(&self.kind.to_be_bytes());
118 out[4..8].copy_from_slice(&self.key_id);
119 out
120 }
121
122 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
128 if bytes.len() != 8 {
129 return Err("CryptoTransformIdentifier needs 8 bytes");
130 }
131 let mut k = [0u8; 4];
132 k.copy_from_slice(&bytes[0..4]);
133 let kind = CryptoTransformKind::from_be_bytes(k)?;
134 let mut key_id = [0u8; 4];
135 key_id.copy_from_slice(&bytes[4..8]);
136 Ok(Self { kind, key_id })
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct CryptoHeader {
151 pub transformation_id: CryptoTransformIdentifier,
153 pub session_id: [u8; 4],
155 pub init_vector_suffix: [u8; 8],
157}
158
159impl CryptoHeader {
160 pub const WIRE_SIZE: usize = 20;
162
163 #[must_use]
165 pub fn to_bytes(&self) -> [u8; Self::WIRE_SIZE] {
166 let mut out = [0u8; Self::WIRE_SIZE];
167 out[0..8].copy_from_slice(&self.transformation_id.to_bytes());
168 out[8..12].copy_from_slice(&self.session_id);
169 out[12..20].copy_from_slice(&self.init_vector_suffix);
170 out
171 }
172
173 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
178 if bytes.len() < Self::WIRE_SIZE {
179 return Err("CryptoHeader needs 20 bytes");
180 }
181 let transformation_id = CryptoTransformIdentifier::from_bytes(&bytes[0..8])?;
182 let mut session_id = [0u8; 4];
183 session_id.copy_from_slice(&bytes[8..12]);
184 let mut iv_suffix = [0u8; 8];
185 iv_suffix.copy_from_slice(&bytes[12..20]);
186 Ok(Self {
187 transformation_id,
188 session_id,
189 init_vector_suffix: iv_suffix,
190 })
191 }
192
193 #[must_use]
196 pub fn full_iv(&self) -> [u8; 12] {
197 let mut iv = [0u8; 12];
198 iv[0..4].copy_from_slice(&self.session_id);
199 iv[4..12].copy_from_slice(&self.init_vector_suffix);
200 iv
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct CryptoFooter {
208 pub common_mac: [u8; 16],
210 pub receiver_specific_macs: Vec<([u8; 4], [u8; 16])>,
213}
214
215impl CryptoFooter {
216 #[must_use]
218 pub fn to_bytes(&self) -> Vec<u8> {
219 let mut out = Vec::with_capacity(20 + self.receiver_specific_macs.len() * 20);
220 out.extend_from_slice(&self.common_mac);
221 let n = self.receiver_specific_macs.len() as u32;
222 out.extend_from_slice(&n.to_be_bytes());
223 for (key_id, mac) in &self.receiver_specific_macs {
224 out.extend_from_slice(key_id);
225 out.extend_from_slice(mac);
226 }
227 out
228 }
229
230 pub fn from_bytes(bytes: &[u8]) -> Result<Self, &'static str> {
235 if bytes.len() < 20 {
236 return Err("CryptoFooter needs >= 20 bytes");
237 }
238 let mut common_mac = [0u8; 16];
239 common_mac.copy_from_slice(&bytes[0..16]);
240 let n_buf: [u8; 4] = bytes[16..20].try_into().map_err(|_| "footer count")?;
241 let n = u32::from_be_bytes(n_buf) as usize;
242 let mut pos = 20;
243 let mut receiver_specific_macs = Vec::with_capacity(n);
244 for _ in 0..n {
245 if bytes.len() < pos + 20 {
246 return Err("receiver-specific MAC truncated");
247 }
248 let mut key_id = [0u8; 4];
249 key_id.copy_from_slice(&bytes[pos..pos + 4]);
250 let mut mac = [0u8; 16];
251 mac.copy_from_slice(&bytes[pos + 4..pos + 20]);
252 receiver_specific_macs.push((key_id, mac));
253 pos += 20;
254 }
255 Ok(Self {
256 common_mac,
257 receiver_specific_macs,
258 })
259 }
260}
261
262pub fn negotiate_transform(
269 remote_kinds: &[CryptoTransformKind],
270) -> Result<CryptoTransformKind, alloc::string::String> {
271 let pref = [
273 CryptoTransformKind::Aes256Gcm,
274 CryptoTransformKind::Aes128Gcm,
275 CryptoTransformKind::Aes256Gmac,
276 CryptoTransformKind::Aes128Gmac,
277 ];
278 for p in pref {
279 if remote_kinds.contains(&p) {
280 return Ok(p);
281 }
282 }
283 Err(format!(
284 "no common crypto transform with peer (peer-offered: {remote_kinds:?})"
285 ))
286}
287
288#[cfg(test)]
289#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn kind_round_trip_all_variants() {
295 for k in [
296 CryptoTransformKind::None,
297 CryptoTransformKind::Aes128Gmac,
298 CryptoTransformKind::Aes128Gcm,
299 CryptoTransformKind::Aes256Gmac,
300 CryptoTransformKind::Aes256Gcm,
301 ] {
302 assert_eq!(
303 CryptoTransformKind::from_be_bytes(k.to_be_bytes()).unwrap(),
304 k
305 );
306 }
307 }
308
309 #[test]
310 fn unknown_kind_rejected() {
311 assert!(CryptoTransformKind::from_be_bytes([0, 0, 0, 99]).is_err());
312 }
313
314 #[test]
315 fn key_sizes_match_spec() {
316 assert_eq!(CryptoTransformKind::Aes128Gcm.key_size(), 16);
317 assert_eq!(CryptoTransformKind::Aes256Gcm.key_size(), 32);
318 assert_eq!(CryptoTransformKind::None.key_size(), 0);
319 }
320
321 #[test]
322 fn tag_size_is_16_for_all_aead_variants() {
323 for k in [
324 CryptoTransformKind::Aes128Gmac,
325 CryptoTransformKind::Aes128Gcm,
326 CryptoTransformKind::Aes256Gmac,
327 CryptoTransformKind::Aes256Gcm,
328 ] {
329 assert_eq!(k.tag_size(), 16);
330 }
331 }
332
333 #[test]
334 fn encrypts_only_for_gcm() {
335 assert!(CryptoTransformKind::Aes128Gcm.encrypts());
336 assert!(CryptoTransformKind::Aes256Gcm.encrypts());
337 assert!(!CryptoTransformKind::Aes128Gmac.encrypts());
338 assert!(!CryptoTransformKind::None.encrypts());
339 }
340
341 #[test]
342 fn transform_identifier_round_trip() {
343 let id = CryptoTransformIdentifier::new(
344 CryptoTransformKind::Aes256Gcm,
345 [0xCA, 0xFE, 0xBA, 0xBE],
346 );
347 let bytes = id.to_bytes();
348 assert_eq!(bytes.len(), 8);
349 let back = CryptoTransformIdentifier::from_bytes(&bytes).unwrap();
350 assert_eq!(back, id);
351 }
352
353 #[test]
354 fn header_round_trip_with_full_iv() {
355 let h = CryptoHeader {
356 transformation_id: CryptoTransformIdentifier::new(
357 CryptoTransformKind::Aes256Gcm,
358 [1, 2, 3, 4],
359 ),
360 session_id: [10, 20, 30, 40],
361 init_vector_suffix: [50, 60, 70, 80, 90, 100, 110, 120],
362 };
363 let bytes = h.to_bytes();
364 assert_eq!(bytes.len(), CryptoHeader::WIRE_SIZE);
365 let back = CryptoHeader::from_bytes(&bytes).unwrap();
366 assert_eq!(back, h);
367 assert_eq!(
368 back.full_iv(),
369 [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
370 );
371 }
372
373 #[test]
374 fn header_short_buffer_rejected() {
375 assert!(CryptoHeader::from_bytes(&[0; 10]).is_err());
376 }
377
378 #[test]
379 fn footer_round_trip_no_receivers() {
380 let f = CryptoFooter {
381 common_mac: [0xAA; 16],
382 receiver_specific_macs: alloc::vec![],
383 };
384 let bytes = f.to_bytes();
385 let back = CryptoFooter::from_bytes(&bytes).unwrap();
386 assert_eq!(back, f);
387 }
388
389 #[test]
390 fn footer_round_trip_with_receivers() {
391 let f = CryptoFooter {
392 common_mac: [0xAA; 16],
393 receiver_specific_macs: alloc::vec![
394 ([1, 2, 3, 4], [0xBB; 16]),
395 ([5, 6, 7, 8], [0xCC; 16]),
396 ],
397 };
398 let bytes = f.to_bytes();
399 let back = CryptoFooter::from_bytes(&bytes).unwrap();
400 assert_eq!(back, f);
401 }
402
403 #[test]
404 fn footer_short_buffer_rejected() {
405 assert!(CryptoFooter::from_bytes(&[0; 10]).is_err());
406 }
407
408 #[test]
409 fn negotiate_picks_strongest_common() {
410 let r = negotiate_transform(&[
411 CryptoTransformKind::Aes128Gmac,
412 CryptoTransformKind::Aes256Gcm,
413 CryptoTransformKind::Aes128Gcm,
414 ])
415 .unwrap();
416 assert_eq!(r, CryptoTransformKind::Aes256Gcm);
417 }
418
419 #[test]
420 fn negotiate_falls_back_to_gmac_if_no_gcm() {
421 let r = negotiate_transform(&[CryptoTransformKind::Aes128Gmac]).unwrap();
422 assert_eq!(r, CryptoTransformKind::Aes128Gmac);
423 }
424
425 #[test]
426 fn negotiate_fails_with_no_overlap() {
427 assert!(negotiate_transform(&[CryptoTransformKind::None]).is_err());
428 assert!(negotiate_transform(&[]).is_err());
429 }
430
431 #[test]
432 fn builtin_plugin_id_matches_spec() {
433 assert_eq!(BUILTIN_CRYPTO_PLUGIN, "DDS:Crypto:AES_GCM_GMAC");
435 }
436}