zoe_app_primitives/
invitation.rs

1//! Group invitation utilities and emoji verification
2//!
3//! This module provides utilities for the group invitation flow, including
4//! the cryptographic emoji derivation function used for PQXDH verification.
5
6use blake3::Hasher;
7
8/// 64 carefully chosen emojis for maximum visual distinction
9///
10/// These emojis are organized into categories to avoid confusion and ensure
11/// cross-platform consistency. They are chosen for:
12/// - Visual distinction (no similar-looking emojis)
13/// - Cross-platform consistency (common emojis that render similarly)
14/// - Accessibility (high contrast, distinct shapes)
15/// - Cultural neutrality (avoid emojis with cultural/religious significance)
16pub const EMOJI_SET: [&str; 64] = [
17    // Objects & Symbols (16)
18    "🔑", "🌟", "🚀", "🎯", "🌈", "🔒", "⚡", "🎨", "🌸", "🔥", "💎", "🎪", "🌊", "🎭", "🍀", "🌺",
19    // Animals & Nature (16)
20    "🐱", "🐶", "🦋", "🐸", "🦊", "🐧", "🦁", "🐯", "🐨", "🐼", "🦉", "🐺", "🦄", "🐙", "🦀", "🐢",
21    // Food & Drinks (16)
22    "🍎", "🍌", "🍇", "🍓", "🥝", "🍑", "🥕", "🌽", "🍄", "🥑", "🍕", "🍔", "🎂", "🍪", "☕", "🍯",
23    // Activities & Objects (16)
24    "⚽", "🏀", "🎸", "🎹", "🎲", "🎮", "📱", "💻", "⌚", "📷", "🎧", "🔍", "💡", "🔧", "⚖️", "🎁",
25];
26
27/// Derive a 6-emoji verification sequence from a PQXDH shared secret
28///
29/// This function takes a 32-byte shared secret and derives a sequence of 6 emojis
30/// that can be displayed to users for manual verification. The derivation uses
31/// BLAKE3 with domain separation to ensure the emojis cannot be used to recover
32/// the original shared secret.
33///
34/// # Security Properties
35///
36/// - **One-way function**: BLAKE3 is cryptographically one-way
37/// - **Domain separation**: Uses unique context string for verification
38/// - **Limited exposure**: Only 48 bits of derived data used for emojis
39/// - **Uniform distribution**: Each emoji has equal probability (1/64)
40/// - **High collision resistance**: 64^6 = 68.7 billion possible sequences
41///
42/// # Algorithm
43///
44/// 1. Derive 32-byte fingerprint using BLAKE3 with domain separation
45/// 2. Split fingerprint into 6 chunks of ~5.33 bytes each
46/// 3. Convert each chunk to little-endian integer
47/// 4. Map integer modulo 64 to emoji index
48///
49/// # Arguments
50///
51/// * `shared_secret` - 32-byte PQXDH shared secret
52///
53/// # Returns
54///
55/// Array of 6 emoji strings for user verification
56///
57/// # Example
58///
59/// ```rust
60/// use zoe_app_primitives::invitation::derive_emoji_verification;
61///
62/// let shared_secret = [0u8; 32]; // Example shared secret
63/// let emojis = derive_emoji_verification(&shared_secret);
64/// println!("Verify these emojis match: {}", emojis.join(" "));
65/// ```
66pub fn derive_emoji_verification(shared_secret: &[u8; 32]) -> [&'static str; 6] {
67    // Derive verification fingerprint using BLAKE3 with domain separation
68    let mut hasher = Hasher::new();
69    hasher.update(shared_secret);
70    hasher.update(b"PQXDH-VERIFICATION-FINGERPRINT-v1");
71    let verification_fingerprint = hasher.finalize();
72    let fingerprint_bytes = verification_fingerprint.as_bytes();
73
74    // Split into 6 chunks and derive emoji for each chunk
75    let mut emojis = [""; 6];
76    for (i, emoji) in emojis.iter_mut().enumerate() {
77        let start = i * 5;
78        let end = std::cmp::min(start + 5, 32);
79        let chunk = &fingerprint_bytes[start..end];
80
81        // Combine bytes in chunk to get index (little-endian)
82        let mut index = 0u64;
83        for (j, &byte) in chunk.iter().enumerate() {
84            index += (byte as u64) << (j * 8);
85        }
86
87        *emoji = EMOJI_SET[(index % 64) as usize];
88    }
89
90    emojis
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_emoji_derivation_deterministic() {
99        let shared_secret = [42u8; 32];
100
101        let emojis1 = derive_emoji_verification(&shared_secret);
102        let emojis2 = derive_emoji_verification(&shared_secret);
103
104        assert_eq!(emojis1, emojis2, "Emoji derivation should be deterministic");
105    }
106
107    #[test]
108    fn test_emoji_derivation_different_secrets() {
109        let secret1 = [1u8; 32];
110        let secret2 = [2u8; 32];
111
112        let emojis1 = derive_emoji_verification(&secret1);
113        let emojis2 = derive_emoji_verification(&secret2);
114
115        assert_ne!(
116            emojis1, emojis2,
117            "Different secrets should produce different emojis"
118        );
119    }
120
121    #[test]
122    fn test_emoji_set_size() {
123        assert_eq!(
124            EMOJI_SET.len(),
125            64,
126            "Emoji set should contain exactly 64 emojis"
127        );
128    }
129
130    #[test]
131    fn test_emoji_set_uniqueness() {
132        let mut unique_emojis = std::collections::HashSet::new();
133        for emoji in &EMOJI_SET {
134            assert!(
135                unique_emojis.insert(emoji),
136                "Emoji set should not contain duplicates: {emoji}"
137            );
138        }
139    }
140    #[test]
141    fn test_domain_separation() {
142        let shared_secret = [100u8; 32];
143
144        // Test that different domain strings produce different results
145        let mut hasher1 = Hasher::new();
146        hasher1.update(&shared_secret);
147        hasher1.update(b"PQXDH-VERIFICATION-FINGERPRINT-v1");
148        let fingerprint1 = hasher1.finalize();
149
150        let mut hasher2 = Hasher::new();
151        hasher2.update(&shared_secret);
152        hasher2.update(b"DIFFERENT-DOMAIN-STRING");
153        let fingerprint2 = hasher2.finalize();
154
155        assert_ne!(
156            fingerprint1.as_bytes(),
157            fingerprint2.as_bytes(),
158            "Different domain strings should produce different fingerprints"
159        );
160    }
161}