#![forbid(unsafe_code)]
use crate::aead;
pub const CHECKIN_KEY_LEN: usize = 16;
const NONCE_LEN: usize = 13; const MIC_LEN: usize = 16;
const COUNTER_LEN: usize = 4;
const MIN_PAYLOAD: usize = NONCE_LEN + COUNTER_LEN + MIC_LEN;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CheckinError {
#[error("check-in payload too short")]
TooShort,
#[error("check-in decryption/authentication failed")]
AuthFailed,
#[error("check-in nonce mismatch")]
NonceMismatch,
#[error("check-in encode failed")]
EncodeFailed,
}
fn checkin_nonce(key: &[u8; CHECKIN_KEY_LEN], counter: u32) -> [u8; NONCE_LEN] {
let hk = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, key);
let tag = ring::hmac::sign(&hk, &counter.to_le_bytes());
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(&tag.as_ref()[..NONCE_LEN]);
nonce
}
pub fn encode_checkin(
key: &[u8; CHECKIN_KEY_LEN],
counter: u32,
app_data: &[u8],
) -> Result<Vec<u8>, CheckinError> {
let nonce = checkin_nonce(key, counter);
let mut plaintext = Vec::with_capacity(COUNTER_LEN + app_data.len());
plaintext.extend_from_slice(&counter.to_le_bytes());
plaintext.extend_from_slice(app_data);
let ct_tag =
aead::encrypt(key, &nonce, &[], &plaintext).map_err(|_| CheckinError::EncodeFailed)?;
let mut out = Vec::with_capacity(NONCE_LEN + ct_tag.len());
out.extend_from_slice(&nonce);
out.extend_from_slice(&ct_tag);
Ok(out)
}
pub fn decode_checkin(
key: &[u8; CHECKIN_KEY_LEN],
payload: &[u8],
) -> Result<(u32, Vec<u8>), CheckinError> {
if payload.len() < MIN_PAYLOAD {
return Err(CheckinError::TooShort);
}
let (nonce_bytes, ct_tag) = payload.split_at(NONCE_LEN);
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(nonce_bytes);
let mut plaintext =
aead::decrypt(key, &nonce, &[], ct_tag).map_err(|_| CheckinError::AuthFailed)?;
if plaintext.len() < COUNTER_LEN {
return Err(CheckinError::TooShort);
}
let mut c = [0u8; COUNTER_LEN];
c.copy_from_slice(&plaintext[..COUNTER_LEN]);
let counter = u32::from_le_bytes(c);
if checkin_nonce(key, counter) != nonce {
return Err(CheckinError::NonceMismatch);
}
plaintext.drain(..COUNTER_LEN);
Ok((counter, plaintext))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)] use super::*;
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
const KEY1: &str = "d90e13180d00baadd20cf5ed4913d3ff";
const PAYLOAD1: &str = "4580d2c6f1310dc4eb64f1f8e8bdc21fb5195d747dd2879b2b0d43ce5b1c565078";
fn key16(hex: &str) -> [u8; 16] {
let mut k = [0u8; 16];
k.copy_from_slice(&unhex(hex));
k
}
#[test]
fn encode_matches_chip_vector1() {
assert_eq!(
encode_checkin(&key16(KEY1), 12, &[]).unwrap(),
unhex(PAYLOAD1)
);
}
#[test]
fn decode_matches_chip_vector1() {
let (counter, app) = decode_checkin(&key16(KEY1), &unhex(PAYLOAD1)).unwrap();
assert_eq!(counter, 12);
assert!(app.is_empty());
}
#[test]
fn decode_rejects_wrong_key() {
assert!(matches!(
decode_checkin(&[0u8; 16], &unhex(PAYLOAD1)),
Err(CheckinError::AuthFailed)
));
}
#[test]
fn decode_rejects_too_short() {
assert!(matches!(
decode_checkin(&key16(KEY1), &[0u8; 10]),
Err(CheckinError::TooShort)
));
}
#[test]
fn roundtrip_with_app_data() {
let key = [0x11u8; 16];
let payload = encode_checkin(&key, 0x0102_0304, b"This").unwrap();
let (c, app) = decode_checkin(&key, &payload).unwrap();
assert_eq!(c, 0x0102_0304);
assert_eq!(app, b"This");
}
}