#![cfg(feature = "json")]
#![allow(clippy::expect_used, clippy::unwrap_used)]
use libpep::data::json::builder::PEPJSONBuilder;
use libpep::data::traits::{Encryptable, Encrypted, Transcryptable};
use libpep::factors::contexts::{EncryptionContext, PseudonymizationDomain};
use libpep::factors::secrets::{EncryptionSecret, PseudonymizationSecret};
use libpep::factors::TranscryptionInfo;
use libpep::keys::{make_global_keys, make_session_keys};
use libpep::pep_json;
#[cfg(feature = "batch")]
use libpep::transcryptor::transcrypt_batch;
use serde_json::json;
#[test]
fn test_json_transcryption_with_macro() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudo_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let enc_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let domain_a = PseudonymizationDomain::from("domain-a");
let domain_b = PseudonymizationDomain::from("domain-b");
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &enc_secret);
let patient_record = pep_json!({
"patient_id": pseudonym("patient-12345"),
"diagnosis": "Flu",
"temperature": 38.5
});
let encrypted = patient_record.encrypt(&session_keys, &mut rng);
#[cfg(feature = "elgamal3")]
let decrypted_original = encrypted.decrypt(&session_keys).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_original = encrypted.decrypt(&session_keys);
let json_original = decrypted_original
.to_value()
.expect("Should convert to JSON");
assert_eq!(json_original["patient_id"], "patient-12345");
assert_eq!(json_original["diagnosis"], "Flu");
let transcryption_info = TranscryptionInfo::new(
&domain_a,
&domain_b,
&session,
&session,
&pseudo_secret,
&enc_secret,
);
let transcrypted = encrypted.transcrypt(&transcryption_info);
assert_ne!(
format!("{:?}", encrypted),
format!("{:?}", transcrypted),
"Encrypted values should be different after transcryption"
);
}
#[test]
fn test_json_transcryption_with_builder() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudo_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let enc_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let domain_a = PseudonymizationDomain::from("clinic-a");
let domain_b = PseudonymizationDomain::from("clinic-b");
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &enc_secret);
let patient_data = json!({
"user_id": "user-67890",
"name": "Alice",
"age": 30,
"active": true
});
let patient_record = PEPJSONBuilder::from_json(&patient_data, &["user_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let encrypted = patient_record.encrypt(&session_keys, &mut rng);
#[cfg(feature = "elgamal3")]
let decrypted_original = encrypted.decrypt(&session_keys).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_original = encrypted.decrypt(&session_keys);
let json_original = decrypted_original
.to_value()
.expect("Should convert to JSON");
assert_eq!(json_original["user_id"], "user-67890");
assert_eq!(json_original["name"], "Alice");
assert_eq!(json_original["age"], 30);
assert_eq!(json_original["active"], true);
let transcryption_info = TranscryptionInfo::new(
&domain_a,
&domain_b,
&session,
&session,
&pseudo_secret,
&enc_secret,
);
let transcrypted = encrypted.transcrypt(&transcryption_info);
#[cfg(feature = "elgamal3")]
let decrypted_transcrypted = transcrypted.decrypt(&session_keys).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_transcrypted = transcrypted.decrypt(&session_keys);
let json_transcrypted = decrypted_transcrypted
.to_value()
.expect("Should convert to JSON");
assert_eq!(json_transcrypted["name"], "Alice");
assert_eq!(json_transcrypted["age"], 30);
assert_eq!(json_transcrypted["active"], true);
assert_ne!(
json_transcrypted["user_id"], "user-67890",
"Pseudonym should be different after cross-domain transcryption"
);
}
#[cfg(feature = "batch")]
#[test]
fn test_json_batch_transcryption_same_structure() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudo_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let enc_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let domain_a = PseudonymizationDomain::from("domain-a");
let domain_b = PseudonymizationDomain::from("domain-b");
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &enc_secret);
let data1 = json!({
"patient_id": "patient-001",
"diagnosis": "Flu",
"temperature": 38.5
});
let data2 = json!({
"patient_id": "patient-002",
"diagnosis": "Cold",
"temperature": 37.2
});
let record1 = PEPJSONBuilder::from_json(&data1, &["patient_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let record2 = PEPJSONBuilder::from_json(&data2, &["patient_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let encrypted1 = record1.encrypt(&session_keys, &mut rng);
let encrypted2 = record2.encrypt(&session_keys, &mut rng);
let structure1 = encrypted1.structure();
let structure2 = encrypted2.structure();
assert_eq!(structure1, structure2, "Records should have same structure");
let transcryption_info = TranscryptionInfo::new(
&domain_a,
&domain_b,
&session,
&session,
&pseudo_secret,
&enc_secret,
);
let mut batch = vec![encrypted1.clone(), encrypted2.clone()];
let transcrypted_batch = transcrypt_batch(&mut batch, &transcryption_info, &mut rng)
.unwrap()
.into_vec();
assert_eq!(transcrypted_batch.len(), 2);
assert_ne!(
format!("{:?}", vec![encrypted1, encrypted2]),
format!("{:?}", transcrypted_batch),
"Batch transcryption should transform the values"
);
let mut decrypted_batch: Vec<serde_json::Value> = transcrypted_batch
.iter()
.map(|v| {
#[cfg(feature = "elgamal3")]
let decrypted = v.decrypt(&session_keys).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted = v.decrypt(&session_keys);
decrypted.to_value().expect("Should convert to JSON")
})
.collect();
decrypted_batch.sort_by(|a, b| {
let temp_a = a["temperature"].as_f64().unwrap();
let temp_b = b["temperature"].as_f64().unwrap();
temp_a.partial_cmp(&temp_b).unwrap()
});
assert_eq!(decrypted_batch[0]["diagnosis"], "Cold");
assert_eq!(decrypted_batch[0]["temperature"].as_f64().unwrap(), 37.2);
assert_ne!(
decrypted_batch[0]["patient_id"], "patient-002",
"Patient ID should be different after cross-domain transcryption"
);
assert_eq!(decrypted_batch[1]["diagnosis"], "Flu");
assert_eq!(decrypted_batch[1]["temperature"].as_f64().unwrap(), 38.5);
assert_ne!(
decrypted_batch[1]["patient_id"], "patient-001",
"Patient ID should be different after cross-domain transcryption"
);
}
#[cfg(feature = "batch")]
#[test]
fn test_json_batch_transcryption_different_structures() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudo_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let enc_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let domain_a = PseudonymizationDomain::from("domain-a");
let domain_b = PseudonymizationDomain::from("domain-b");
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &enc_secret);
let data1 = json!({
"patient_id": "patient-001",
"diagnosis": "Flu",
"temperature": 38.5
});
let data2 = json!({
"user_id": "user-002",
"name": "Bob",
"age": 25,
"active": true
});
let record1 = PEPJSONBuilder::from_json(&data1, &["patient_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let record2 = PEPJSONBuilder::from_json(&data2, &["user_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let encrypted1 = record1.encrypt(&session_keys, &mut rng);
let encrypted2 = record2.encrypt(&session_keys, &mut rng);
let structure1 = encrypted1.structure();
let structure2 = encrypted2.structure();
assert_ne!(
structure1, structure2,
"Records should have different structures"
);
let transcryption_info = TranscryptionInfo::new(
&domain_a,
&domain_b,
&session,
&session,
&pseudo_secret,
&enc_secret,
);
let mut batch = vec![encrypted1, encrypted2];
let result = transcrypt_batch(&mut batch, &transcryption_info, &mut rng);
assert!(result.is_err(), "Should fail with inconsistent structures");
match result {
Err(libpep::transcryptor::BatchError::InconsistentStructure { .. }) => {
}
_ => panic!("Expected InconsistentStructure error"),
}
}
#[test]
fn test_json_transcryption_with_client_and_transcryptor() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudo_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let enc_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let domain_a = PseudonymizationDomain::from("domain-a");
let domain_b = PseudonymizationDomain::from("domain-b");
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &enc_secret);
let client = libpep::client::Client::new(session_keys);
let transcryptor =
libpep::transcryptor::Transcryptor::new(pseudo_secret.clone(), enc_secret.clone());
let patient_data = json!({
"patient_id": "patient-54321",
"name": "John Doe",
"diagnosis": "Healthy",
"temperature": 36.6
});
let patient_record = PEPJSONBuilder::from_json(&patient_data, &["patient_id"])
.expect("Should create PEP JSON from existing JSON")
.build();
let encrypted = client.encrypt(&patient_record, &mut rng);
#[cfg(feature = "elgamal3")]
let decrypted_original = client.decrypt(&encrypted).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_original = client.decrypt(&encrypted);
let json_original = decrypted_original
.to_value()
.expect("Should convert to JSON");
assert_eq!(json_original["patient_id"], "patient-54321");
assert_eq!(json_original["name"], "John Doe");
assert_eq!(json_original["diagnosis"], "Healthy");
assert_eq!(json_original["temperature"].as_f64().unwrap(), 36.6);
let transcryption_info =
transcryptor.transcryption_info(&domain_a, &domain_b, &session, &session);
let transcrypted = transcryptor.transcrypt(&encrypted, &transcryption_info);
assert_ne!(
format!("{:?}", encrypted),
format!("{:?}", transcrypted),
"Encrypted values should be different after transcryption"
);
#[cfg(feature = "elgamal3")]
let decrypted_transcrypted = client.decrypt(&transcrypted).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_transcrypted = client.decrypt(&transcrypted);
let json_transcrypted = decrypted_transcrypted
.to_value()
.expect("Should convert to JSON");
assert_eq!(json_transcrypted["name"], "John Doe");
assert_eq!(json_transcrypted["diagnosis"], "Healthy");
assert_eq!(json_transcrypted["temperature"].as_f64().unwrap(), 36.6);
assert_ne!(
json_transcrypted["patient_id"], "patient-54321",
"Pseudonym should be different after cross-domain transcryption"
);
}
#[test]
fn test_pseudonym_roundtrip_with_json_serialization() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudonymization_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let encryption_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &encryption_secret);
let client = libpep::client::Client::new(session_keys);
let transcryptor = libpep::transcryptor::Transcryptor::new(
pseudonymization_secret.clone(),
encryption_secret.clone(),
);
let original_pep = pep_json!({
"short_id": pseudonym("john"), "long_id": pseudonym("user@example.com"), "name": "Alice",
"age": 30
});
let encrypted = client.encrypt(&original_pep, &mut rng);
let domain_from = PseudonymizationDomain::from("domain-a");
let domain_to = PseudonymizationDomain::from("domain-b");
let transcryption_info =
transcryptor.transcryption_info(&domain_from, &domain_to, &session, &session);
let transcrypted = transcryptor.transcrypt(&encrypted, &transcryption_info);
#[cfg(feature = "elgamal3")]
let decrypted = client.decrypt(&transcrypted).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted = client.decrypt(&transcrypted);
let json_value = decrypted.to_value().unwrap();
assert_eq!(json_value["name"], "Alice");
assert_eq!(json_value["age"], 30);
let short_id_hex = json_value["short_id"].as_str().unwrap();
let long_id_hex = json_value["long_id"].as_str().unwrap();
assert_eq!(
short_id_hex.len(),
64,
"Short pseudonym should be 64 hex chars"
);
assert_eq!(
long_id_hex.len(),
128,
"Long pseudonym should be 128 hex chars"
);
let transcrypted_pep = pep_json!({
"short_id": pseudonym(short_id_hex), "long_id": pseudonym(long_id_hex), "name": "Alice",
"age": 30
});
let encrypted_b = client.encrypt(&transcrypted_pep, &mut rng);
let transcryption_info_back =
transcryptor.transcryption_info(&domain_to, &domain_from, &session, &session);
let transcrypted_back = transcryptor.transcrypt(&encrypted_b, &transcryption_info_back);
#[cfg(feature = "elgamal3")]
let decrypted_back = client.decrypt(&transcrypted_back).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_back = client.decrypt(&transcrypted_back);
let json_back = decrypted_back.to_value().unwrap();
assert_eq!(json_back["short_id"], "john");
assert_eq!(json_back["long_id"], "user@example.com");
assert_eq!(json_back["name"], "Alice");
assert_eq!(json_back["age"], 30);
}
#[test]
fn test_pseudonym_roundtrip_with_builder() {
use libpep::data::json::data::PEPJSONValue;
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudonymization_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let encryption_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &encryption_secret);
let client = libpep::client::Client::new(session_keys);
let transcryptor = libpep::transcryptor::Transcryptor::new(
pseudonymization_secret.clone(),
encryption_secret.clone(),
);
let original_pep = PEPJSONBuilder::new()
.pseudonym("short_id", "john") .pseudonym("long_id", "user@example.com") .attribute("name", json!("Alice"))
.attribute("age", json!(30))
.build();
if let PEPJSONValue::Object(fields) = &original_pep {
assert!(matches!(
fields.get("short_id"),
Some(PEPJSONValue::Pseudonym(_))
));
assert!(
matches!(fields.get("long_id"), Some(PEPJSONValue::LongPseudonym(lp)) if lp.len() == 2)
);
}
let encrypted = client.encrypt(&original_pep, &mut rng);
let domain_from = PseudonymizationDomain::from("domain-a");
let domain_to = PseudonymizationDomain::from("domain-b");
let transcryption_info =
transcryptor.transcryption_info(&domain_from, &domain_to, &session, &session);
let transcrypted = transcryptor.transcrypt(&encrypted, &transcryption_info);
#[cfg(feature = "elgamal3")]
let decrypted = client.decrypt(&transcrypted).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted = client.decrypt(&transcrypted);
let json_value = decrypted.to_value().unwrap();
assert_eq!(json_value["name"], "Alice");
assert_eq!(json_value["age"], 30);
let short_id_hex = json_value["short_id"].as_str().unwrap();
let long_id_hex = json_value["long_id"].as_str().unwrap();
assert_eq!(
short_id_hex.len(),
64,
"Short pseudonym should be 64 hex chars"
);
assert_eq!(
long_id_hex.len(),
128,
"Long pseudonym should be 128 hex chars"
);
let transcrypted_pep = PEPJSONBuilder::new()
.pseudonym("short_id", short_id_hex) .pseudonym("long_id", long_id_hex) .attribute("name", json!("Alice"))
.attribute("age", json!(30))
.build();
if let PEPJSONValue::Object(fields) = &transcrypted_pep {
assert!(matches!(
fields.get("short_id"),
Some(PEPJSONValue::Pseudonym(_))
));
assert!(matches!(
fields.get("long_id"),
Some(PEPJSONValue::LongPseudonym(lp)) if lp.len() == 2
));
}
let encrypted_b = client.encrypt(&transcrypted_pep, &mut rng);
let transcryption_info_back =
transcryptor.transcryption_info(&domain_to, &domain_from, &session, &session);
let transcrypted_back = transcryptor.transcrypt(&encrypted_b, &transcryption_info_back);
#[cfg(feature = "elgamal3")]
let decrypted_back = client.decrypt(&transcrypted_back).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_back = client.decrypt(&transcrypted_back);
let json_back = decrypted_back.to_value().unwrap();
assert_eq!(json_back["short_id"], "john");
assert_eq!(json_back["long_id"], "user@example.com");
assert_eq!(json_back["name"], "Alice");
assert_eq!(json_back["age"], 30);
}
#[test]
fn test_unicode_pseudonyms_and_attributes() {
let mut rng = rand::rng();
let (_global_public, global_secret) = make_global_keys(&mut rng);
let pseudonymization_secret = PseudonymizationSecret::from("pseudo-secret".as_bytes().to_vec());
let encryption_secret = EncryptionSecret::from("encryption-secret".as_bytes().to_vec());
let session = EncryptionContext::from("session-1");
let session_keys = make_session_keys(&global_secret, &session, &encryption_secret);
let client = libpep::client::Client::new(session_keys);
let transcryptor = libpep::transcryptor::Transcryptor::new(
pseudonymization_secret.clone(),
encryption_secret.clone(),
);
let original_pep = pep_json!({
"emoji_id": pseudonym("🔒👤"), "chinese_id": pseudonym("用户@例子.中国"), "arabic_name": "مرحبا بك", "cyrillic_name": "Здравствуй", "mixed": "Café™ ñoño 你好 🏥", "age": 25
});
let encrypted = client.encrypt(&original_pep, &mut rng);
let domain_from = PseudonymizationDomain::from("domain-a");
let domain_to = PseudonymizationDomain::from("domain-b");
let transcryption_info =
transcryptor.transcryption_info(&domain_from, &domain_to, &session, &session);
let transcrypted = transcryptor.transcrypt(&encrypted, &transcryption_info);
#[cfg(feature = "elgamal3")]
let decrypted = client.decrypt(&transcrypted).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted = client.decrypt(&transcrypted);
let json_value = decrypted.to_value().unwrap();
assert_eq!(json_value["arabic_name"], "مرحبا بك");
assert_eq!(json_value["cyrillic_name"], "Здравствуй");
assert_eq!(json_value["mixed"], "Café™ ñoño 你好 🏥");
assert_eq!(json_value["age"], 25);
let emoji_id_hex = json_value["emoji_id"].as_str().unwrap();
let chinese_id_hex = json_value["chinese_id"].as_str().unwrap();
assert_eq!(
emoji_id_hex.len(),
64,
"Emoji pseudonym should be 64 hex chars"
);
assert!(
chinese_id_hex.len() >= 64,
"Chinese email should need at least 1 block"
);
assert_eq!(
chinese_id_hex.len() % 64,
0,
"Should be multiple of 64 chars"
);
let transcrypted_pep = pep_json!({
"emoji_id": pseudonym(emoji_id_hex),
"chinese_id": pseudonym(chinese_id_hex),
"arabic_name": "مرحبا بك",
"cyrillic_name": "Здравствуй",
"mixed": "Café™ ñoño 你好 🏥",
"age": 25
});
let encrypted_b = client.encrypt(&transcrypted_pep, &mut rng);
let transcryption_info_back =
transcryptor.transcryption_info(&domain_to, &domain_from, &session, &session);
let transcrypted_back = transcryptor.transcrypt(&encrypted_b, &transcryption_info_back);
#[cfg(feature = "elgamal3")]
let decrypted_back = client.decrypt(&transcrypted_back).unwrap();
#[cfg(not(feature = "elgamal3"))]
let decrypted_back = client.decrypt(&transcrypted_back);
let json_back = decrypted_back.to_value().unwrap();
assert_eq!(json_back["emoji_id"], "🔒👤");
assert_eq!(json_back["chinese_id"], "用户@例子.中国");
assert_eq!(json_back["arabic_name"], "مرحبا بك");
assert_eq!(json_back["cyrillic_name"], "Здравствуй");
assert_eq!(json_back["mixed"], "Café™ ñoño 你好 🏥");
assert_eq!(json_back["age"], 25);
}