1#![forbid(unsafe_code)]
11
12use crate::aead;
13
14pub const CHECKIN_KEY_LEN: usize = 16;
16
17const NONCE_LEN: usize = 13; const MIC_LEN: usize = 16;
19const COUNTER_LEN: usize = 4;
20const MIN_PAYLOAD: usize = NONCE_LEN + COUNTER_LEN + MIC_LEN; #[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum CheckinError {
26 #[error("check-in payload too short")]
28 TooShort,
29 #[error("check-in decryption/authentication failed")]
31 AuthFailed,
32 #[error("check-in nonce mismatch")]
34 NonceMismatch,
35 #[error("check-in encode failed")]
37 EncodeFailed,
38}
39
40fn checkin_nonce(key: &[u8; CHECKIN_KEY_LEN], counter: u32) -> [u8; NONCE_LEN] {
43 let hk = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, key);
44 let tag = ring::hmac::sign(&hk, &counter.to_le_bytes());
45 let mut nonce = [0u8; NONCE_LEN];
46 nonce.copy_from_slice(&tag.as_ref()[..NONCE_LEN]);
47 nonce
48}
49
50pub fn encode_checkin(
56 key: &[u8; CHECKIN_KEY_LEN],
57 counter: u32,
58 app_data: &[u8],
59) -> Result<Vec<u8>, CheckinError> {
60 let nonce = checkin_nonce(key, counter);
61 let mut plaintext = Vec::with_capacity(COUNTER_LEN + app_data.len());
62 plaintext.extend_from_slice(&counter.to_le_bytes());
63 plaintext.extend_from_slice(app_data);
64 let ct_tag =
65 aead::encrypt(key, &nonce, &[], &plaintext).map_err(|_| CheckinError::EncodeFailed)?;
66 let mut out = Vec::with_capacity(NONCE_LEN + ct_tag.len());
67 out.extend_from_slice(&nonce);
68 out.extend_from_slice(&ct_tag);
69 Ok(out)
70}
71
72pub fn decode_checkin(
78 key: &[u8; CHECKIN_KEY_LEN],
79 payload: &[u8],
80) -> Result<(u32, Vec<u8>), CheckinError> {
81 if payload.len() < MIN_PAYLOAD {
82 return Err(CheckinError::TooShort);
83 }
84 let (nonce_bytes, ct_tag) = payload.split_at(NONCE_LEN);
85 let mut nonce = [0u8; NONCE_LEN];
86 nonce.copy_from_slice(nonce_bytes);
87 let plaintext =
88 aead::decrypt(key, &nonce, &[], ct_tag).map_err(|_| CheckinError::AuthFailed)?;
89 if plaintext.len() < COUNTER_LEN {
90 return Err(CheckinError::TooShort);
91 }
92 let mut c = [0u8; COUNTER_LEN];
93 c.copy_from_slice(&plaintext[..COUNTER_LEN]);
94 let counter = u32::from_le_bytes(c);
95 if checkin_nonce(key, counter) != nonce {
97 return Err(CheckinError::NonceMismatch);
98 }
99 Ok((counter, plaintext[COUNTER_LEN..].to_vec()))
100}
101
102#[cfg(test)]
103mod tests {
104 #![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
106
107 fn unhex(s: &str) -> Vec<u8> {
108 (0..s.len())
109 .step_by(2)
110 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
111 .collect()
112 }
113
114 const KEY1: &str = "d90e13180d00baadd20cf5ed4913d3ff";
116 const PAYLOAD1: &str = "4580d2c6f1310dc4eb64f1f8e8bdc21fb5195d747dd2879b2b0d43ce5b1c565078";
117
118 fn key16(hex: &str) -> [u8; 16] {
119 let mut k = [0u8; 16];
120 k.copy_from_slice(&unhex(hex));
121 k
122 }
123
124 #[test]
125 fn encode_matches_chip_vector1() {
126 assert_eq!(
127 encode_checkin(&key16(KEY1), 12, &[]).unwrap(),
128 unhex(PAYLOAD1)
129 );
130 }
131
132 #[test]
133 fn decode_matches_chip_vector1() {
134 let (counter, app) = decode_checkin(&key16(KEY1), &unhex(PAYLOAD1)).unwrap();
135 assert_eq!(counter, 12);
136 assert!(app.is_empty());
137 }
138
139 #[test]
140 fn decode_rejects_wrong_key() {
141 assert!(matches!(
142 decode_checkin(&[0u8; 16], &unhex(PAYLOAD1)),
143 Err(CheckinError::AuthFailed)
144 ));
145 }
146
147 #[test]
148 fn decode_rejects_too_short() {
149 assert!(matches!(
150 decode_checkin(&key16(KEY1), &[0u8; 10]),
151 Err(CheckinError::TooShort)
152 ));
153 }
154
155 #[test]
156 fn roundtrip_with_app_data() {
157 let key = [0x11u8; 16];
158 let payload = encode_checkin(&key, 0x0102_0304, b"This").unwrap();
159 let (c, app) = decode_checkin(&key, &payload).unwrap();
160 assert_eq!(c, 0x0102_0304);
161 assert_eq!(app, b"This");
162 }
163}