Pseudonymization¶
Pseudonymizer replaces values with tokens that can be turned back into the original by
whoever holds the key. It offers two operations with deliberately different security
properties.
Reversible tokens¶
pseudonymize encrypts with Fernet, the same primitive behind
Symmetric. Every call produces a different token for the same input, so
tokens cannot be correlated with each other.
from shadow_data.reversible import Pseudonymizer
pseudonymizer = Pseudonymizer()
token = pseudonymizer.pseudonymize('user@example.com')
print(token)
print(pseudonymizer.depseudonymize(token))
# The same value encrypts differently every time.
print(pseudonymizer.pseudonymize('user@example.com') != token)
Key handling¶
A key is generated when none is supplied. Read it from key and store it somewhere safe —
tokens are worthless without it, and there is no recovery path.
from shadow_data.reversible import Pseudonymizer
pseudonymizer = Pseudonymizer()
key = pseudonymizer.key # b'...' — store this, and not in source control
token = pseudonymizer.pseudonymize('user@example.com')
# A later process reverses the token with the stored key.
print(Pseudonymizer(key).depseudonymize(token))
The key is the whole security boundary
Anyone with the key can reverse every token you ever issued. Keep it in a secrets manager or environment variable, never in the repository, and never in logs.
depseudonymize raises InvalidPseudonymError for a token produced by a different key, a
malformed token, or a deterministic token. Constructing a Pseudonymizer with an invalid
key raises InvalidCipherKeyError immediately, rather than failing later at first use.
Deterministic tokens¶
deterministic_pseudonym returns a stable HMAC-SHA256 token: the same input always maps to
the same token under the same key. That is what allows records to be joined across
datasets.
from shadow_data.reversible import Pseudonymizer
pseudonymizer = Pseudonymizer()
orders = ['ana@example.com', 'bob@example.com', 'ana@example.com']
print([pseudonymizer.deterministic_pseudonym(email)[:12] for email in orders])
The first and third orders are visibly the same customer. That is the point of the operation — and also its risk.
Deterministic tokens are linkable by design
Repeated values stay visible as repeated tokens, so the shape of your data leaks even when the values do not. And because the mapping is stable, anyone holding the key can confirm a guessed value by recomputing its token — a realistic attack for low-entropy inputs such as email addresses or national identifiers, where the space of plausible guesses is small.
Prefer pseudonymize unless you actually need the join.
Deterministic tokens are one-way. They cannot be reversed, not even with the key:
from shadow_data.exceptions import InvalidPseudonymError
from shadow_data.reversible import Pseudonymizer
pseudonymizer = Pseudonymizer()
token = pseudonymizer.deterministic_pseudonym('secret')
try:
pseudonymizer.depseudonymize(token)
except InvalidPseudonymError as error:
print(error)
Why the two modes never share key material¶
Both operations are driven by the same key you hold, but the HMAC subkey is derived from
the Fernet key with HKDF-SHA256 under a fixed context string
(shadow_data.reversible.deterministic.v1). Reusing one key directly for two different
primitives is a well-known footgun; domain-separating them means compromising one does not
directly expose the other.
The derivation is deterministic, so the same key always yields the same subkey — which is what makes deterministic tokens stable across processes and machines.