tap_msg/utils/
name_hash.rs

1//! Name hashing utilities for TAIP-12 compliance
2//!
3//! This module provides functionality for hashing participant names according to TAIP-12,
4//! which enables privacy-preserving Travel Rule compliance by sharing hashed names instead
5//! of plaintext names.
6
7use sha2::{Digest, Sha256};
8
9/// Trait for types that can generate a hashed name according to TAIP-12
10pub trait NameHashable {
11    /// Generate a SHA-256 hash of the name according to TAIP-12 normalization rules
12    ///
13    /// The normalization process:
14    /// 1. Remove all whitespace characters
15    /// 2. Convert to uppercase
16    /// 3. Encode as UTF-8
17    /// 4. Hash with SHA-256
18    /// 5. Return as lowercase hex string
19    ///
20    /// # Arguments
21    ///
22    /// * `name` - The name to hash (can be a person's full name or organization name)
23    ///
24    /// # Returns
25    ///
26    /// A 64-character lowercase hex string representing the SHA-256 hash
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// use tap_msg::utils::name_hash::NameHashable;
32    ///
33    /// struct Person;
34    /// impl NameHashable for Person {}
35    ///
36    /// let hash = Person::hash_name("Alice Lee");
37    /// assert_eq!(hash, "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e");
38    /// ```
39    fn hash_name(name: &str) -> String {
40        // Normalize: remove whitespace and convert to uppercase
41        let normalized = name
42            .chars()
43            .filter(|c| !c.is_whitespace())
44            .collect::<String>()
45            .to_uppercase();
46
47        // Hash with SHA-256
48        let mut hasher = Sha256::new();
49        hasher.update(normalized.as_bytes());
50        let result = hasher.finalize();
51
52        // Convert to lowercase hex string
53        hex::encode(result)
54    }
55}
56
57/// Generate a TAIP-12 compliant name hash
58///
59/// This is a standalone function that implements the same algorithm as the trait method.
60///
61/// # Arguments
62///
63/// * `name` - The name to hash
64///
65/// # Returns
66///
67/// A 64-character lowercase hex string representing the SHA-256 hash
68pub fn hash_name(name: &str) -> String {
69    struct Hasher;
70    impl NameHashable for Hasher {}
71    Hasher::hash_name(name)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_hash_name_basic() {
80        // Test case from TAIP-12 specification
81        let hash = hash_name("Alice Lee");
82        assert_eq!(
83            hash,
84            "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e"
85        );
86    }
87
88    #[test]
89    fn test_hash_name_bob_smith() {
90        // Test case from TAIP-12 specification
91        let hash = hash_name("Bob Smith");
92        assert_eq!(
93            hash,
94            "5432e86b4d4a3a2b4be57b713b12c5c576c88459fe1cfdd760fd6c99a0e06686"
95        );
96    }
97
98    #[test]
99    fn test_hash_name_normalization() {
100        // All these should produce the same hash
101        let expected = hash_name("ALICELEE");
102        assert_eq!(hash_name("Alice Lee"), expected);
103        assert_eq!(hash_name("alice lee"), expected);
104        assert_eq!(hash_name("ALICE LEE"), expected);
105        assert_eq!(hash_name("Alice  Lee"), expected);
106        assert_eq!(hash_name(" Alice Lee "), expected);
107        assert_eq!(hash_name("Alice\tLee"), expected);
108        assert_eq!(hash_name("Alice\nLee"), expected);
109    }
110
111    #[test]
112    fn test_hash_name_with_middle_name() {
113        let hash = hash_name("Alice Marie Lee");
114        // Should normalize to "ALICEMARIELEE"
115        assert_eq!(hash.len(), 64); // SHA-256 produces 32 bytes = 64 hex chars
116    }
117
118    #[test]
119    fn test_hash_name_organization() {
120        let hash = hash_name("Example VASP Ltd.");
121        // Should normalize to "EXAMPLEVASPLTD"
122        assert_eq!(hash.len(), 64);
123    }
124
125    #[test]
126    fn test_hash_name_special_characters() {
127        // Note: TAIP-12 only removes whitespace, not punctuation
128        let hash1 = hash_name("O'Brien");
129        let hash2 = hash_name("OBrien");
130        assert_ne!(hash1, hash2); // These should be different
131    }
132
133    #[test]
134    fn test_hash_name_unicode() {
135        let hash = hash_name("José García");
136        // Should normalize to "JOSÉBGARCÍA" (preserving accented characters)
137        assert_eq!(hash.len(), 64);
138    }
139
140    #[test]
141    fn test_trait_implementation() {
142        struct TestHasher;
143        impl NameHashable for TestHasher {}
144
145        let hash = TestHasher::hash_name("Alice Lee");
146        assert_eq!(
147            hash,
148            "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e"
149        );
150    }
151}