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}