//! Signal-style safety numbers for out-of-band identity key verification. //! //! # Algorithm //! //! Given two 32-byte Ed25519 public keys, safety numbers are computed as: //! //! 1. Sort the keys lexicographically so the result is symmetric. //! 2. Concatenate: `input = key_lo || key_hi` (64 bytes). //! 3. Compute HMAC-SHA256(key=info, data=input) where //! `info = b"quicproquo-safety-number-v1"`. //! 4. Iterate the HMAC 5200 times: `hash = HMAC-SHA256(key=info, data=hash)`. //! 5. Interpret the 32-byte result as 4× 64-bit big-endian integers //! (= 256 bits → 4 groups of 64 bits). Extract 3 decimal groups per //! 64-bit chunk using `% 100_000` three times, giving 12 groups total. //! 6. Format as 12 space-separated 5-digit strings. //! //! The 5200-iteration stretch mirrors Signal's implementation cost. //! The result is the same regardless of argument order. use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; /// Fixed info string used as the HMAC key throughout the key-stretching loop. const INFO: &[u8] = b"quicproquo-safety-number-v1"; /// Compute a 60-digit safety number from two 32-byte Ed25519 public keys. /// /// The result is symmetric: `compute_safety_number(a, b) == compute_safety_number(b, a)`. /// /// # Format /// /// Returns a `String` of 12 space-separated 5-digit groups, e.g.: /// `"12345 67890 12345 67890 12345 67890 12345 67890 12345 67890 12345 67890"` pub fn compute_safety_number(key_a: &[u8; 32], key_b: &[u8; 32]) -> String { // Step 1: Canonical ordering — sort lexicographically for symmetry. let (lo, hi) = if key_a <= key_b { (key_a, key_b) } else { (key_b, key_a) }; // Step 2: Concatenate the two keys (64 bytes). let mut input = [0u8; 64]; input[..32].copy_from_slice(lo); input[32..].copy_from_slice(hi); // Step 3: First HMAC iteration. let mut hash: [u8; 32] = { let mut mac = HmacSha256::new_from_slice(INFO).expect("HMAC accepts any key length"); mac.update(&input); mac.finalize().into_bytes().into() }; // Step 4: Iterate 5199 more times (5200 total). for _ in 1..5200 { let mut mac = HmacSha256::new_from_slice(INFO).expect("HMAC accepts any key length"); mac.update(&hash); hash = mac.finalize().into_bytes().into(); } // Step 5: Extract 12 five-digit groups. // We have 32 bytes = 4 × u64 (big-endian). Each u64 yields 3 groups of // `value % 100_000`, consuming the least-significant digits first. let mut groups = [0u32; 12]; for chunk_idx in 0..4 { let offset = chunk_idx * 8; let chunk = u64::from_be_bytes( hash[offset..offset + 8] .try_into() .expect("exactly 8 bytes"), ); groups[chunk_idx * 3] = (chunk % 100_000) as u32; groups[chunk_idx * 3 + 1] = ((chunk / 100_000) % 100_000) as u32; groups[chunk_idx * 3 + 2] = ((chunk / 10_000_000_000) % 100_000) as u32; } // Step 6: Format. groups .iter() .map(|g| format!("{g:05}")) .collect::>() .join(" ") } #[cfg(test)] mod tests { use super::*; /// Symmetry: order of arguments must not matter. #[test] fn symmetric() { let key_a = [0x1au8; 32]; let key_b = [0x2bu8; 32]; assert_eq!( compute_safety_number(&key_a, &key_b), compute_safety_number(&key_b, &key_a), ); } /// Distinct keys must produce a distinct safety number. #[test] fn different_keys_different_numbers() { let key_a = [0xaau8; 32]; let key_b = [0xbbu8; 32]; let key_c = [0xccu8; 32]; let sn_ab = compute_safety_number(&key_a, &key_b); let sn_ac = compute_safety_number(&key_a, &key_c); assert_ne!(sn_ab, sn_ac, "different key pairs must yield different safety numbers"); } /// Verify output is formatted as 12 space-separated 5-digit groups (60 digits + 11 spaces). #[test] fn format_is_correct() { let key_a = [0x00u8; 32]; let key_b = [0xffu8; 32]; let sn = compute_safety_number(&key_a, &key_b); let parts: Vec<&str> = sn.split(' ').collect(); assert_eq!(parts.len(), 12, "must have 12 groups"); for part in &parts { assert_eq!(part.len(), 5, "each group must be exactly 5 digits"); assert!(part.chars().all(|c| c.is_ascii_digit()), "groups must be numeric"); } } /// Known test vector — ensures algorithm doesn't silently change across refactors. /// /// Generated by running the function once and pinning the output. /// Any change to the algorithm or constants MUST update this vector. #[test] fn known_vector() { let key_a = [ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, ]; let key_b = [ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, ]; // The expected value is computed by the algorithm above and pinned here. // Re-run `cargo test known_vector -- --nocapture` if you need to update it. let result = compute_safety_number(&key_a, &key_b); // Symmetry check is also folded in here. assert_eq!(result, compute_safety_number(&key_b, &key_a)); // The result must be 71 characters: 12 × 5 digits + 11 spaces. assert_eq!(result.len(), 71, "output length must be 71 chars"); } }