Skip to content

Commit e049c0c

Browse files
committed
Include pii hash and lei by default
1 parent 0f80562 commit e049c0c

File tree

4 files changed

+289
-71
lines changed

4 files changed

+289
-71
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Demonstration of PII hashing for TAP transfers
2+
3+
use tap_msg::message::Party;
4+
use tap_msg::utils::NameHashable;
5+
6+
fn main() {
7+
println!("TAP Transfer PII Hashing Demonstration\n");
8+
9+
// Example 1: Natural Person - Alice Lee
10+
println!("1. Natural Person Example:");
11+
println!(" Name: Alice Lee");
12+
13+
let alice = Party::new("did:example:alice")
14+
.with_name_hash("Alice Lee")
15+
.with_country("US");
16+
17+
println!(" Party JSON:");
18+
let alice_json = serde_json::to_string_pretty(&alice).unwrap();
19+
println!("{}", alice_json);
20+
21+
// Example 2: Organization - Example Corp
22+
println!("\n2. Organization Example:");
23+
println!(" Legal Name: Example Corporation Ltd.");
24+
println!(" LEI: 549300ZFEEJ2IP5VME73");
25+
26+
let corp = Party::new("did:web:example.com")
27+
.with_lei("549300ZFEEJ2IP5VME73")
28+
.with_country("US");
29+
30+
// For organizations, we could also add legal name in metadata
31+
let mut corp_with_name = corp;
32+
corp_with_name.add_metadata(
33+
"legalName".to_string(),
34+
serde_json::Value::String("Example Corporation Ltd.".to_string()),
35+
);
36+
37+
println!(" Party JSON:");
38+
let corp_json = serde_json::to_string_pretty(&corp_with_name).unwrap();
39+
println!("{}", corp_json);
40+
41+
// Example 3: Show the difference
42+
println!("\n3. Privacy Comparison:");
43+
println!(" Without hashing (DO NOT USE IN PRODUCTION):");
44+
let mut alice_with_pii = Party::new("did:example:alice");
45+
alice_with_pii.add_metadata("givenName".to_string(), serde_json::json!("Alice"));
46+
alice_with_pii.add_metadata("familyName".to_string(), serde_json::json!("Lee"));
47+
println!("{}", serde_json::to_string_pretty(&alice_with_pii).unwrap());
48+
49+
println!("\n With hashing (RECOMMENDED):");
50+
println!("{}", alice_json);
51+
52+
// Example 4: Name hash verification
53+
println!("\n4. Name Hash Verification:");
54+
struct Hasher;
55+
impl NameHashable for Hasher {}
56+
57+
let hash1 = Hasher::hash_name("Alice Lee");
58+
let hash2 = Hasher::hash_name("alice lee"); // Different case
59+
let hash3 = Hasher::hash_name("Alice Lee"); // Extra space
60+
61+
println!(" 'Alice Lee' -> {}", hash1);
62+
println!(" 'alice lee' -> {}", hash2);
63+
println!(" 'Alice Lee' -> {}", hash3);
64+
println!(" All hashes match: {}", hash1 == hash2 && hash2 == hash3);
65+
}

tap-mcp/src/tools/transaction_tools.rs

Lines changed: 115 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tap_msg::message::tap_message_trait::TapMessageBody;
1414
use tap_msg::message::{
1515
Agent, Authorize, Cancel, Complete, Party, Reject, Revert, Settle, Transfer,
1616
};
17+
use tap_node::storage::models::SchemaType;
1718
use tracing::{debug, error};
1819

1920
/// Tool for creating transfer transactions
@@ -123,44 +124,66 @@ impl ToolHandler for CreateTransferTool {
123124
if let Some(_profile) = customer.profile.as_object() {
124125
let mut metadata = HashMap::new();
125126

126-
// Add name information if available
127-
if let Some(given_name) = customer.given_name {
128-
metadata.insert(
129-
"givenName".to_string(),
130-
serde_json::Value::String(given_name),
131-
);
132-
}
133-
if let Some(family_name) = customer.family_name {
134-
metadata.insert(
135-
"familyName".to_string(),
136-
serde_json::Value::String(family_name),
137-
);
138-
}
139-
if let Some(display_name) = customer.display_name {
140-
metadata.insert("name".to_string(), serde_json::Value::String(display_name));
141-
}
127+
match customer.schema_type {
128+
SchemaType::Person => {
129+
// For natural persons, use name hash instead of PII
130+
let full_name = match (&customer.given_name, &customer.family_name) {
131+
(Some(given), Some(family)) => format!("{} {}", given, family),
132+
(Some(given), None) => given.clone(),
133+
(None, Some(family)) => family.clone(),
134+
(None, None) => customer.display_name.clone().unwrap_or_default(),
135+
};
136+
137+
if !full_name.is_empty() {
138+
// Add name hash according to TAIP-12
139+
originator = originator.with_name_hash(&full_name);
140+
}
142141

143-
// Add address information if available
144-
if let Some(country) = customer.address_country {
145-
metadata.insert(
146-
"addressCountry".to_string(),
147-
serde_json::Value::String(country),
148-
);
149-
}
150-
if let Some(locality) = customer.address_locality {
151-
metadata.insert(
152-
"addressLocality".to_string(),
153-
serde_json::Value::String(locality),
154-
);
155-
}
156-
if let Some(postal_code) = customer.postal_code {
157-
metadata.insert(
158-
"postalCode".to_string(),
159-
serde_json::Value::String(postal_code),
160-
);
142+
// Add address information if available (still needed for compliance)
143+
if let Some(country) = customer.address_country {
144+
metadata.insert(
145+
"addressCountry".to_string(),
146+
serde_json::Value::String(country),
147+
);
148+
}
149+
}
150+
SchemaType::Organization => {
151+
// For organizations, include LEI code if available
152+
if let Some(lei_code) = customer.lei_code {
153+
originator = originator.with_lei(&lei_code);
154+
}
155+
156+
// Add legal name for organizations
157+
if let Some(legal_name) = customer.legal_name {
158+
metadata.insert(
159+
"legalName".to_string(),
160+
serde_json::Value::String(legal_name),
161+
);
162+
}
163+
164+
// Add address information if available
165+
if let Some(country) = customer.address_country {
166+
metadata.insert(
167+
"addressCountry".to_string(),
168+
serde_json::Value::String(country),
169+
);
170+
}
171+
}
172+
SchemaType::Thing => {
173+
// For other entity types, include minimal metadata
174+
if let Some(display_name) = customer.display_name {
175+
metadata.insert(
176+
"name".to_string(),
177+
serde_json::Value::String(display_name),
178+
);
179+
}
180+
}
161181
}
162182

163-
originator = Party::with_metadata(&originator.id, metadata);
183+
// Apply any additional metadata
184+
if !metadata.is_empty() {
185+
originator = Party::with_metadata(&originator.id, metadata);
186+
}
164187
}
165188
}
166189
// Also merge any provided metadata
@@ -180,44 +203,66 @@ impl ToolHandler for CreateTransferTool {
180203
if let Some(_profile) = customer.profile.as_object() {
181204
let mut metadata = HashMap::new();
182205

183-
// Add name information if available
184-
if let Some(given_name) = customer.given_name {
185-
metadata.insert(
186-
"givenName".to_string(),
187-
serde_json::Value::String(given_name),
188-
);
189-
}
190-
if let Some(family_name) = customer.family_name {
191-
metadata.insert(
192-
"familyName".to_string(),
193-
serde_json::Value::String(family_name),
194-
);
195-
}
196-
if let Some(display_name) = customer.display_name {
197-
metadata.insert("name".to_string(), serde_json::Value::String(display_name));
198-
}
206+
match customer.schema_type {
207+
SchemaType::Person => {
208+
// For natural persons, use name hash instead of PII
209+
let full_name = match (&customer.given_name, &customer.family_name) {
210+
(Some(given), Some(family)) => format!("{} {}", given, family),
211+
(Some(given), None) => given.clone(),
212+
(None, Some(family)) => family.clone(),
213+
(None, None) => customer.display_name.clone().unwrap_or_default(),
214+
};
215+
216+
if !full_name.is_empty() {
217+
// Add name hash according to TAIP-12
218+
beneficiary = beneficiary.with_name_hash(&full_name);
219+
}
199220

200-
// Add address information if available
201-
if let Some(country) = customer.address_country {
202-
metadata.insert(
203-
"addressCountry".to_string(),
204-
serde_json::Value::String(country),
205-
);
206-
}
207-
if let Some(locality) = customer.address_locality {
208-
metadata.insert(
209-
"addressLocality".to_string(),
210-
serde_json::Value::String(locality),
211-
);
212-
}
213-
if let Some(postal_code) = customer.postal_code {
214-
metadata.insert(
215-
"postalCode".to_string(),
216-
serde_json::Value::String(postal_code),
217-
);
221+
// Add address information if available (still needed for compliance)
222+
if let Some(country) = customer.address_country {
223+
metadata.insert(
224+
"addressCountry".to_string(),
225+
serde_json::Value::String(country),
226+
);
227+
}
228+
}
229+
SchemaType::Organization => {
230+
// For organizations, include LEI code if available
231+
if let Some(lei_code) = customer.lei_code {
232+
beneficiary = beneficiary.with_lei(&lei_code);
233+
}
234+
235+
// Add legal name for organizations
236+
if let Some(legal_name) = customer.legal_name {
237+
metadata.insert(
238+
"legalName".to_string(),
239+
serde_json::Value::String(legal_name),
240+
);
241+
}
242+
243+
// Add address information if available
244+
if let Some(country) = customer.address_country {
245+
metadata.insert(
246+
"addressCountry".to_string(),
247+
serde_json::Value::String(country),
248+
);
249+
}
250+
}
251+
SchemaType::Thing => {
252+
// For other entity types, include minimal metadata
253+
if let Some(display_name) = customer.display_name {
254+
metadata.insert(
255+
"name".to_string(),
256+
serde_json::Value::String(display_name),
257+
);
258+
}
259+
}
218260
}
219261

220-
beneficiary = Party::with_metadata(&beneficiary.id, metadata);
262+
// Apply any additional metadata
263+
if !metadata.is_empty() {
264+
beneficiary = Party::with_metadata(&beneficiary.id, metadata);
265+
}
221266
}
222267
}
223268
// Also merge any provided metadata

tap-mcp/tests/test_pii_hashing.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! Test for PII hashing functionality in transfers
2+
3+
use serde_json::json;
4+
use tap_node::storage::models::{Customer, SchemaType};
5+
6+
#[tokio::test]
7+
async fn test_transfer_with_person_uses_name_hash() {
8+
// Create a test setup with a person customer
9+
let customer = Customer {
10+
id: "did:example:alice".to_string(),
11+
agent_did: "did:example:agent".to_string(),
12+
schema_type: SchemaType::Person,
13+
given_name: Some("Alice".to_string()),
14+
family_name: Some("Lee".to_string()),
15+
display_name: Some("Alice Lee".to_string()),
16+
legal_name: None,
17+
lei_code: None,
18+
mcc_code: None,
19+
address_country: Some("US".to_string()),
20+
address_locality: None,
21+
postal_code: None,
22+
street_address: None,
23+
profile: json!({
24+
"@type": "Person",
25+
"givenName": "Alice",
26+
"familyName": "Lee"
27+
}),
28+
ivms101_data: None,
29+
verified_at: None,
30+
created_at: chrono::Utc::now().to_rfc3339(),
31+
updated_at: chrono::Utc::now().to_rfc3339(),
32+
};
33+
34+
// The expected name hash for "Alice Lee" according to TAIP-12
35+
let expected_name_hash = "b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e";
36+
37+
// TODO: Complete test implementation once we have a proper test setup
38+
// This would need:
39+
// 1. Mock TAP integration
40+
// 2. Mock storage that returns our test customer
41+
// 3. Call CreateTransferTool with the customer's DID
42+
// 4. Verify the resulting transfer has nameHash instead of PII
43+
44+
assert_eq!(expected_name_hash.len(), 64); // SHA-256 produces 64 hex chars
45+
}
46+
47+
#[tokio::test]
48+
async fn test_transfer_with_organization_uses_lei_code() {
49+
// Create a test setup with an organization customer
50+
let customer = Customer {
51+
id: "did:web:example.com".to_string(),
52+
agent_did: "did:example:agent".to_string(),
53+
schema_type: SchemaType::Organization,
54+
given_name: None,
55+
family_name: None,
56+
display_name: Some("Example Corp".to_string()),
57+
legal_name: Some("Example Corporation Ltd.".to_string()),
58+
lei_code: Some("549300ZFEEJ2IP5VME73".to_string()), // Example LEI
59+
mcc_code: Some("5812".to_string()),
60+
address_country: Some("US".to_string()),
61+
address_locality: Some("New York".to_string()),
62+
postal_code: Some("10001".to_string()),
63+
street_address: None,
64+
profile: json!({
65+
"@type": "Organization",
66+
"legalName": "Example Corporation Ltd.",
67+
"leiCode": "549300ZFEEJ2IP5VME73"
68+
}),
69+
ivms101_data: None,
70+
verified_at: None,
71+
created_at: chrono::Utc::now().to_rfc3339(),
72+
updated_at: chrono::Utc::now().to_rfc3339(),
73+
};
74+
75+
// TODO: Complete test implementation
76+
// This would verify that:
77+
// 1. LEI code is included in the transfer
78+
// 2. Legal name is included
79+
// 3. No name hash is generated for organizations
80+
81+
assert_eq!(customer.lei_code.unwrap().len(), 20); // LEI codes are 20 chars
82+
}
83+
84+
#[test]
85+
fn test_name_hash_generation() {
86+
use tap_msg::utils::NameHashable;
87+
88+
struct TestHasher;
89+
impl NameHashable for TestHasher {}
90+
91+
// Test cases from TAIP-12
92+
let hash1 = TestHasher::hash_name("Alice Lee");
93+
assert_eq!(
94+
hash1,
95+
"b117f44426c9670da91b563db728cd0bc8bafa7d1a6bb5e764d1aad2ca25032e"
96+
);
97+
98+
let hash2 = TestHasher::hash_name("Bob Smith");
99+
assert_eq!(
100+
hash2,
101+
"5432e86b4d4a3a2b4be57b713b12c5c576c88459fe1cfdd760fd6c99a0e06686"
102+
);
103+
104+
// Test normalization
105+
assert_eq!(TestHasher::hash_name("ALICE LEE"), hash1);
106+
assert_eq!(TestHasher::hash_name("alice lee"), hash1);
107+
assert_eq!(TestHasher::hash_name("Alice Lee"), hash1);
108+
}

tap-node/benches/stress_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async fn create_test_message(
2424
) -> (PlainMessage, Transfer) {
2525
// Create a simple transfer message
2626
let body = Transfer {
27-
transaction_id: uuid::Uuid::new_v4().to_string(),
27+
transaction_id: Some(uuid::Uuid::new_v4().to_string()),
2828
asset: tap_caip::AssetId::from_str(
2929
"eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f",
3030
)

0 commit comments

Comments
 (0)