Skip to main content

hap_crypto/
setup.rs

1//! The HAP setup hash: the value that binds a scanned setup id to a specific
2//! accessory's device id (used for QR → device matching).
3
4use sha2::{Digest, Sha512};
5
6/// The 4-byte HAP **setup hash**: the first four bytes of
7/// `SHA-512(setup_id ‖ device_id)`, hashing the two ASCII strings back to back
8/// with no separator.
9///
10/// `setup_id` is the 4-character id from a setup payload; `device_id` is the
11/// accessory's device id **exactly as the accessory advertises it** — HAP
12/// canonical form is uppercase colon-hex (e.g. `"AA:BB:CC:DD:EE:FF"`). The hash
13/// is case-sensitive; the caller is responsible for passing the canonical case
14/// (see the matcher, which uppercases before calling this).
15#[must_use]
16pub fn setup_hash(setup_id: &str, device_id: &str) -> [u8; 4] {
17    let mut h = Sha512::new();
18    h.update(setup_id.as_bytes());
19    h.update(device_id.as_bytes());
20    let digest = h.finalize();
21    let mut out = [0u8; 4];
22    out.copy_from_slice(&digest[..4]);
23    out
24}
25
26#[cfg(test)]
27#[allow(clippy::unwrap_used)] // test-code carve-out: a failed unwrap is a test failure
28mod tests {
29    use super::setup_hash;
30
31    #[test]
32    fn matches_hap_setup_hash_vector() {
33        // test-vectors/setup-hash/onvis-style.json
34        assert_eq!(
35            setup_hash("7OSX", "AA:BB:CC:DD:EE:FF"),
36            [0x5c, 0x8a, 0x27, 0x40]
37        );
38    }
39
40    #[test]
41    fn is_case_sensitive_on_device_id() {
42        // The hash is over exact bytes — lowercasing the device id changes it.
43        assert_ne!(
44            setup_hash("7OSX", "AA:BB:CC:DD:EE:FF"),
45            setup_hash("7OSX", "aa:bb:cc:dd:ee:ff")
46        );
47    }
48}