1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//! Stable stand-ins for identifiers.
//!
//! Clearing an identifier destroys the message as test data: nothing joins
//! the patient in this message to the same patient in the next one. A
//! pseudonym replaces the identifier with a token that is the same
//! everywhere the identifier was, for a given key.
//!
//! **This is not a cryptographic guarantee** (D12). Read
//! [`pseudonym()`]'s documentation, and spec §7.3, before using it on data
//! that leaves your control.
//!
//! Specified by spec §7.
/// The FNV-1a 64-bit offset basis and prime.
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
/// A stable pseudonym for `value` under `key`: sixteen lowercase
/// hexadecimal characters.
///
/// # Stability
///
/// The value returned is **frozen** (spec §13.2). The same key and value
/// give the same pseudonym on every platform and in every future release
/// of this crate, major versions included, because a pseudonym is a join
/// key: a data set redacted last year and a message redacted today have to
/// still agree about which patient is which.
///
/// # What it does not give you
///
/// The construction is FNV-1a over the key bytes followed by the value
/// bytes. It is a hash, not a message authentication code, and it leaks
/// two things on purpose and one by accident:
///
/// - **Equality**, by construction. Anyone can see which messages concern
/// the same patient and count how many each generated — which, joined
/// with an outside data set, can re-identify the largest one.
/// - **Everything, to anyone who can guess.** Record numbers come from
/// small spaces. Given the key, an attacker computes the pseudonym of
/// every candidate in seconds and inverts the mapping completely.
///
/// So: use this inside your own trust boundary — test environments,
/// reproductions, CI fixtures. For data leaving it, use
/// [`Action::Clear`](crate::Action::Clear) or
/// [`Action::Replace`](crate::Action::Replace), which leak nothing but the
/// fact that a value was there.
///
/// Example:
///
/// ```
/// use er7_redact::pseudonym::pseudonym;
///
/// // Stable: the same identifier maps the same way, every time.
/// assert_eq!(pseudonym(0, "PATID1234"), pseudonym(0, "PATID1234"));
/// assert_eq!(pseudonym(0, "PATID1234").len(), 16);
///
/// // Keyed: two data sets redacted under different keys cannot be joined.
/// assert_ne!(pseudonym(0, "PATID1234"), pseudonym(1, "PATID1234"));
///
/// // Distinct: different identifiers do not collide into one patient.
/// assert_ne!(pseudonym(0, "PATID1234"), pseudonym(0, "PATID1235"));
/// ```