Masking¶
Masking keeps a value recognizable to a human while hiding most of it — the last four digits of a card on a receipt, for example. Use anonymization instead when the value should not be recognizable at all.
All helpers live in shadow_data.masking.
Masking never returns its input unchanged
When the window you ask to keep covers the whole value, the whole value is masked instead. For a privacy library, "returned the original data" is the worst possible failure mode, so it is ruled out by construction.
mask¶
Masks every character except the first keep_first and the last keep_last.
from shadow_data.masking import mask
mask('sensitive') # '*********'
mask('sensitive', keep_last=3) # '******ive'
mask('sensitive', keep_first=1, keep_last=1) # 's*******e'
mask('sensitive', keep_last=3, mask_char='#') # '######ive'
When the window covers everything, everything is masked:
ValueError is raised for a negative window, or for a mask_char that is not exactly one
character.
A common use is trimming secrets down to an identifiable prefix:
from shadow_data.masking import mask
print(mask('sk_live_51H8xKzLmNpQrStUv', keep_first=7, keep_last=4))
mask_digits¶
Masks digits while leaving separators — and any other non-digit — in place. Useful for any formatted identifier.
from shadow_data.masking import mask_digits
mask_digits('4111 1111 1111 1111', keep_last=4) # '**** **** **** 1111'
mask_digits('order AB-1234', keep_last=2) # 'order AB-**34'
mask_digits('123-456') # '***-***'
Asking to keep more digits than the value has masks everything:
mask_credit_card¶
Masks a card number, keeping the last 4 digits and the original spacing.
from shadow_data.masking import mask_credit_card
mask_credit_card('4111 1111 1111 1111') # '**** **** **** 1111'
mask_credit_card('4111-1111-1111-1111') # '****-****-****-1111'
mask_credit_card('4111111111111111') # '************1111'
mask_credit_card('378282246310005') # '***********0005'
The value is first checked against the Luhn checksum and a plausible length of 12–19
digits. Anything else raises InvalidCreditCardError, so a mistyped or unrelated value is
never returned partially readable:
from shadow_data.exceptions import InvalidCreditCardError
from shadow_data.masking import mask_credit_card
try:
mask_credit_card('4111111111111112') # fails the checksum
except InvalidCreditCardError as error:
print(error)
passes_luhn is exposed separately if you only need the checksum:
from shadow_data.masking import passes_luhn
passes_luhn('4111111111111111') # True
passes_luhn('4111111111111112') # False
partial_email¶
Keeps the first character of the user and of the first domain label.
from shadow_data.masking import partial_email
partial_email('user@example.com') # 'u***@e******.com'
partial_email('user@example.co.uk') # 'u***@e******.co.uk'
partial_email('a@example.com') # '*@e******.com'
A one-character user or label has nothing to keep, so it is masked entirely. Invalid
addresses raise InvalidEmailError.
Use EmailAnonymization.anonymize_email when the user
part must be hidden completely.
Choosing mask_char¶
Every helper accepts mask_char. The default * is the safe choice for display; pick
something else when the output goes somewhere * is meaningful, such as a Markdown
document or a glob pattern.