use serde_json::{Map, Value, json};
use super::builder::conceal;
use super::disclosure::Disclosure;
use super::tests::{HIDDEN, sample};
#[test]
fn digest_order_does_not_follow_claim_order() {
let (concealed, disclosures) = conceal(&sample(), |name| HIDDEN.contains(&name)).unwrap();
let published: Vec<&str> = concealed["_sd"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
let in_claim_order: Vec<String> = disclosures.iter().map(Disclosure::digest).collect();
let mut sorted = in_claim_order.clone();
sorted.sort();
assert_eq!(published, sorted, "digests must be published sorted");
let reordered = (0..32).any(|_| {
let (c, d) = conceal(&sample(), |name| HIDDEN.contains(&name)).unwrap();
let pub_order: Vec<String> = c["_sd"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
let claim_order: Vec<String> = d.iter().map(Disclosure::digest).collect();
pub_order != claim_order
});
assert!(reordered, "sorting never reordered — suspect a no-op sort");
}
#[test]
fn salts_are_unique_and_128_bits() {
use base64::Engine;
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let (_, disclosures) = conceal(&sample(), |name| HIDDEN.contains(&name)).unwrap();
let mut salts: Vec<&str> = disclosures.iter().map(Disclosure::salt).collect();
let count = salts.len();
salts.sort_unstable();
salts.dedup();
assert_eq!(salts.len(), count, "a salt was reused across claims");
for d in &disclosures {
assert_eq!(b64.decode(d.salt()).unwrap().len(), 16);
}
}
#[test]
fn two_issuances_of_the_same_claims_share_no_digest() {
let (first, _) = conceal(&sample(), |name| HIDDEN.contains(&name)).unwrap();
let (second, _) = conceal(&sample(), |name| HIDDEN.contains(&name)).unwrap();
let a: Vec<&Value> = first["_sd"].as_array().unwrap().iter().collect();
let b: Vec<&Value> = second["_sd"].as_array().unwrap().iter().collect();
assert!(
a.iter().all(|d| !b.contains(d)),
"a digest repeated across two issuances — salts are being reused"
);
}
#[test]
fn the_same_claim_name_at_two_places_gets_two_salts() {
let outer = json!({ "serialNumber": "A" }).as_object().unwrap().clone();
let inner = json!({ "serialNumber": "A" }).as_object().unwrap().clone();
let (_, a) = conceal(&outer, |n| n == "serialNumber").unwrap();
let (_, b) = conceal(&inner, |n| n == "serialNumber").unwrap();
assert_ne!(a[0].salt(), b[0].salt());
assert_ne!(a[0].digest(), b[0].digest());
}
#[test]
fn no_salt_ever_repeats_across_claims_or_issuances() {
use base64::Engine;
use proptest::prelude::*;
use std::collections::HashSet;
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
proptest!(|(
names in prop::collection::hash_set("[a-z][a-z0-9]{0,12}", 1..12),
issuances in 2usize..6,
)| {
let object: Map<String, Value> = names
.iter()
.map(|n| (format!("f_{n}"), json!("v")))
.collect();
let mut seen: HashSet<String> = HashSet::new();
for _ in 0..issuances {
let (_, disclosures) = conceal(&object, |_| true).unwrap();
prop_assert_eq!(disclosures.len(), object.len());
for d in &disclosures {
prop_assert_eq!(
b64.decode(d.salt()).unwrap().len(),
16,
"salt is not 128 bits"
);
}
prop_assert!(
salts_are_all_new(&disclosures, &mut seen),
"a salt repeated — two credentials for the same field are linkable"
);
}
});
}
fn salts_are_all_new(
disclosures: &[Disclosure],
seen: &mut std::collections::HashSet<String>,
) -> bool {
let mut all_new = true;
for d in disclosures {
all_new &= seen.insert(d.salt().to_owned());
}
all_new
}
#[test]
fn the_salt_detector_rejects_a_repeat() {
let repeated = "AAAAAAAAAAAAAAAAAAAAAA".to_owned();
let mut seen = std::collections::HashSet::new();
let first = [Disclosure::with_salt(repeated.clone(), "a", json!(1)).unwrap()];
assert!(
salts_are_all_new(&first, &mut seen),
"the first sighting of a salt is not a repeat"
);
let second = [Disclosure::with_salt(repeated, "b", json!(2)).unwrap()];
assert!(
!salts_are_all_new(&second, &mut seen),
"a repeated salt was accepted — the property test above would pass \
through a broken CSPRNG"
);
let third =
[Disclosure::with_salt("BBBBBBBBBBBBBBBBBBBBBB".to_owned(), "c", json!(3)).unwrap()];
assert!(salts_are_all_new(&third, &mut seen));
}