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 mut 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 plaintext.drain(..COUNTER_LEN);
100 Ok((counter, plaintext))
101}
102
103#[cfg(test)]
104mod tests {
105 #![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
107
108 fn unhex(s: &str) -> Vec<u8> {
109 (0..s.len())
110 .step_by(2)
111 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
112 .collect()
113 }
114
115 const KEY1: &str = "d90e13180d00baadd20cf5ed4913d3ff";
117 const PAYLOAD1: &str = "4580d2c6f1310dc4eb64f1f8e8bdc21fb5195d747dd2879b2b0d43ce5b1c565078";
118
119 fn key16(hex: &str) -> [u8; 16] {
120 let mut k = [0u8; 16];
121 k.copy_from_slice(&unhex(hex));
122 k
123 }
124
125 #[test]
126 fn encode_matches_chip_vector1() {
127 assert_eq!(
128 encode_checkin(&key16(KEY1), 12, &[]).unwrap(),
129 unhex(PAYLOAD1)
130 );
131 }
132
133 #[test]
134 fn decode_matches_chip_vector1() {
135 let (counter, app) = decode_checkin(&key16(KEY1), &unhex(PAYLOAD1)).unwrap();
136 assert_eq!(counter, 12);
137 assert!(app.is_empty());
138 }
139
140 #[test]
141 fn decode_rejects_wrong_key() {
142 assert!(matches!(
143 decode_checkin(&[0u8; 16], &unhex(PAYLOAD1)),
144 Err(CheckinError::AuthFailed)
145 ));
146 }
147
148 #[test]
149 fn decode_rejects_too_short() {
150 assert!(matches!(
151 decode_checkin(&key16(KEY1), &[0u8; 10]),
152 Err(CheckinError::TooShort)
153 ));
154 }
155
156 #[test]
157 fn roundtrip_with_app_data() {
158 let key = [0x11u8; 16];
159 let payload = encode_checkin(&key, 0x0102_0304, b"This").unwrap();
160 let (c, app) = decode_checkin(&key, &payload).unwrap();
161 assert_eq!(c, 0x0102_0304);
162 assert_eq!(app, b"This");
163 }
164}