Skip to content

Masking strategies

A strategy is how a sensitive value gets transformed. All built-in strategies are deterministic: the RNG is seeded from sha256(masking.seed + value), so the same input always maps to the same output — across columns, tables, databases and runs. (The seed map then makes that durable even when inputs to the computation change.)

Catalogue

Strategy Output Notes
fake_name Mary JohnsonSusan Scott consistent first+last from bundled dictionaries
fake_first_name / fake_last_name single name part
fake_city AustinTucson US city list bundled; register your own for other locales
fake_email jane@corp.comkaren.lopez316@example.invalid RFC-reserved .invalid TLD — can never actually deliver
fake_email_keep_domain jane@corp.comkaren.lopez316@corp.com keeps the (possibly identifying) domain — explicit opt-in
fake_uuid valid, deterministic v4 UUID case and {} braces preserved; uuid.UUID in → uuid.UUID out
fake_ip 203.0.113.7141.66.203.9 valid octets (1–254); IPv6 keeps grouping/case
fake_credit_card digits random, separators kept Luhn-valid so checksum-validating consumers keep working
fake_date ±30–730-day deterministic shift always a real calendar date, same representation in/out
format_random Ab3-9zQf7-2k same length + character classes; typed values stay typed (below)
shuffle characters permuted in place separators keep positions; typed values dispatch like format_random
redact Ada-99***-** keeps length/separators
null / blank SQL NULL / '' for fields that shouldn't survive at all

Typed values stay typed — and valid

Values arrive from the driver as Python objects, not strings. format_random and shuffle route them to type-preserving logic:

Input type Behavior
int digits randomized, digit count kept, no leading zero, sign kept
float / Decimal digits randomized in place — still parses (1e-05 keeps its e)
date / datetime delegated to fake_date (a real date, same type)
uuid.UUID delegated to fake_uuid (a uuid.UUID)
bool passed through — one bit has no shape to hide

This is what keeps a masked DATE column from receiving "8342-73-51" and a masked INTEGER column from receiving a string.

Which strategy applies to a column?

First match wins:

flowchart TD
    A["Sensitive column<br/>(detected rule, e.g. email)"] --> B{"1. column_strategies<br/>(your per-column call)"}
    B -- yes --> Z[use it]
    B -- no --> C{"2. rule_strategies<br/>(your per-rule mapping)"}
    C -- yes --> Z
    C -- no --> D{"3. built-in default<br/>for the rule"}
    D -- yes --> Z
    D -- no --> E["4. masking.default_strategy"]

Built-in rule defaults: email→fake_email, full_name→fake_name, first_name/last_name/city → their fakes, uuid→fake_uuid, ip_address→fake_ip, credit_card→fake_credit_card, date/date_of_birth→fake_date, phone/ssn/zip_code→format_random, address→redact.

Free text is your call

Long text fields (notes, comments, support transcripts) can contain anything. Detection flags the column at best — the safe treatments are blunt ones, chosen explicitly:

masking:
  column_strategies:
    notes: blank          # wipe it
    # notes: redact       # keep length, hide content
    # notes: format_random  # scramble it

Custom strategies and dictionaries

from dbmask.masking.rules import register_strategy
from dbmask.masking.dictionaries import register_dictionary
from dbmask.masking.format import seeded_rng

register_dictionary("countries", ["France", "Japan", "Brazil"])

def strat_fixed_suffix(value, ctx):
    if value is None:
        return None                      # masking never invents data
    rng = seeded_rng(str(value), ctx.seed)
    return f"user-{rng.randint(1000, 9999)}"

register_strategy("fixed_suffix", strat_fixed_suffix)

Rules for a well-behaved strategy: deterministic (derive randomness from seeded_rng(value, ctx.seed)), None stays None, and output should be valid for the column's type. dbmask strategies lists everything registered.