Українська версія / Ukrainian version
A tool for consistent masking of personal data in Ukrainian military documents with exact restoration of the original. It recognises names in every grammatical case and word order (including forms of address without a surname), ranks, tax IDs, passports and military IDs, military units, order and BR numbers and dates; one person gets one mask throughout a document, and the mask keeps the grammatical form of the original. Restoration works from a mapping file (optionally encrypted) and does not depend on the algorithm version. Available as a CLI (data-mask / data-unmask), a Python module or a stand-alone Windows build; behaviour is configured in config.yaml and a private config_local.yaml.
Current version — see CHANGELOG.md or python data_masking.py -V.
Since v3.0 all code lives in a single top-level package datamasking
(pip-installable — see INSTALL.md).
data-mask/data-unmask/data-masking-diagnose— console scripts (afterpip install)python -m datamasking mask|unmask [args]— run as a moduledata_masking.py— data masking (thin wrapper overdatamasking.masking)unmask_data.py— unmasking (thin wrapper overdatamasking.unmasking)diagnose_mapping.py— diagnostics, mapping comparison and recovery verification (wrapper overdatamasking.diagnose)__main__.py— run from the repository root:python . mask [args]/python . unmask [args]
Compatibility: the old flat import paths (
import masking,import unmasking,from modules.tools import …,from rank_data import …) still work from a repository checkout via shims, but emit aDeprecationWarning; the shims will be removed in 4.0.0. They are not installed bypip install— usedatamasking.*in new code.
| Module | Description |
|---|---|
constants.py |
MASK_* flags, patterns, name dictionaries, metadata |
helpers.py |
Seed, mapping, instance tracking, normalization |
language.py |
Gender, grammatical case, name declension |
context.py |
Recognition of lines containing PIB (Прізвище Ім'я По-батькові), context parsing |
mask_personal.py |
IPN, passports, surnames, first names, patronymics |
mask_military.py |
Ranks, units, orders, BR, dates |
engine.py |
Main engine: context-aware masking of text/JSON, initials |
cli.py |
CLI, config, reports, main() |
| Module | Description |
|---|---|
helpers.py |
Instance map, file pair lookup, mapping versions |
engine.py |
Text/JSON recovery, re-mask chains |
io.py |
Loading mapping (.json/.enc), schema validation |
cli.py |
CLI, main() |
datamasking/rank_data.py— UAF ranks (army, navy, medical/legal service) with declensions;datamasking/extras/rank_data.py— re-exportdatamasking/_version.py— single source of the package version
| Module | Description |
|---|---|
config.py |
YAML + ENV + CLI configuration with priorities (CLI > ENV > config_local.yaml > config.yaml > config.py > Default) |
security.py |
AES-256-GCM encryption/decryption of mapping files |
masking_logger.py |
Structured logging (JSON + colored console output) |
selective.py |
--only / --exclude filters for selective masking |
re_mask.py |
Chain re-masking (multi-pass) with chain tracking |
tools.py |
Atomic masking functions for programmatic use (API) |
password_generator.py |
Password generator (ASCII, Cyrillic, custom symbols) |
config.yaml— the shared configuration in the repository root, read automatically: every key with its default and the full exclusion lists (dictionaries)config_local.example.yaml— template for your privateconfig_local.yaml(not committed)config_example.yaml/config_example.py— full configuration example with all options and their defaultsdocs/config-examples/— ready-made sets for typical scenarios:share,strict,pii
cd data-masking # repository checkout
pip install ".[full]" # double quotes work in bash, PowerShell and cmd alike
data-mask -i input.txt # console scripts appear on PATH
data-unmask masked_file.txtRunning from source without installation also works — see below.
python data_masking.py -i input.txtResult (in the current directory, or next to -o when given):
output_YYYYMMDD_HHMMSS_NNN.txt— masked textmasking_map_YYYYMMDD_HHMMSS_NNN.json— mapping for unmask (.encwith--encrypt; mode 0600)masking_report_YYYYMMDD_HHMMSS_NNN.txt— report (skip with--no-report)
Automatic mode:
python unmask_data.py masked_file.txtManual mode:
python unmask_data.py masked_file.txt -m mapping_file.jsonWith encrypted mapping file (v2.3.0+):
python unmask_data.py masked_file.txt --map mapping.enc --password mypasswordWith configuration file (v2.3.0+):
python unmask_data.py -c config.yamlResult:
input_recovery_YYYYMMDD_HHMMSS.txtnext to the masked file (or the-opath) — recovered text
python diagnose_mapping.py mapping1.json mapping2.json # compare mappings
python diagnose_mapping.py # in a folder with input.txt + input_recovery_*.txt: verifies the recovery, compares the two latest mappings- PIB (Прізвище Ім'я По-батькові) — with grammatical case and gender awareness
- PIB with initials (v2.5.0+) —
Іванов П.А.,П. Агранов,К.П. Іванов,Т. А. Сидоренко,КОВАЛЕНКО І.В.; initials are preserved in the mapping and restored during unmask - Patronymics — masked while preserving gender and letter case
- IPN — 10 digits
- ID passports — 9 digits (first 3 + last are fixed, middle 5 are random)
- Passports — АА123456, АА №123456
- Military IDs — МТ123456, мт-123456, мт №123456
- Ranks — all grammatical cases, with "retired"/"reserve" modifiers; ranks have the masculine form only, for women as well (
сержанту Коваль Тетяні Сергіївні), and the mask keeps it. A feminine form that happens to be in the input (сержантці) is recognised and restored as it was - Brigades — "123 окрема механізована бригада"
- Military units — "в/ч А1234"
- Order numbers — "наказ №123 від 01.01.2025"
- BR numbers — 75/25/3400/Р, 818/856, 86319/06/1689/Р, 566
- Dates — DD.MM.YYYY and D.M.YYYY (±30 days, never 0; also when glued to letters:
31.12.2024р.); written-out dates («06» жовтня 2025 року) get another day, month and ±1 year
- Abbreviations (never masked as a surname) — ЗСУ, МОУ, ВСУ, ДПСУ, НГУ, ДСНС, СБУ, ГУР, ТЦК, СП, КМУ, ОТЦКСП, ТВО, ТРО
- Words that are never part of a name — legal and service terms (Згідно, Відповідно, Закону, Статуту …), positions (Командир, Начальник …), labels (ПІБ, ІПН, РНОКПП …), "Кабінет Міністрів" in every case
- Position titles and unit names in capitals — three or more words in capitals without a patronymic and without a rank (
МАСКУВАЛЬНИКОМ ІМІТАЦІЙНОГО ВІДДІЛЕННЯ), two words in capitals whose second word is not a known first name (ЗАХИСНИКУ ВІТЧИЗНИ), words whose endings do not fit a name (Імітаційного,Відділення); headings in capitals (НАКАЗ КОМАНДИРА ВІЙСЬКОВОЇ ЧАСТИНИ) - Dates of legal acts — a date after "Закону / Кодексу / Конституції / Указу Президента / постанови Кабінету Міністрів, Верховної Ради, КМУ … від" is not shifted
The full lists are in config.yaml (section dictionaries, editable; the program keeps a built-in copy for when the key is missing); the exclusions section of the configuration adds to them (abbreviations, words, phrases, legal acts, words that are always masked) or removes entries. data-mask --list-exclusions prints what is in effect.
Each occurrence is tracked individually:
Вхід: "Іванов Петро Іванович і Петров Олег Петрович. Потім Іванов пішов, а Петров лишився."
Маска: "Івенов Павло Леонідович і Пергов Омелян Степанович. Потім Івенов пішов, а Пергов лишився."
A surname is recognised as part of a name (Прізвище Ім'я По батькові, Ім'я По батькові Прізвище, or after a rank); once it is known in the document, every other occurrence of it — including other case forms — gets the same synthetic stem, and unmask restores each occurrence by its instance number. A first name with a patronymic and no surname is masked too (3.1.13): Шановний Олегу Петровичу! → Шановний Омеляну Степановичу!, за підписом Олега Петровича → за підписом Омеляна Степановича. A capitalised word in front of a full name (Заява Петренка Олега Петровича, Характеристика Коваль Тетяни Сергіївни) is left alone and the name after it is masked in full.
Position titles and unit names written in capitals are not names (3.1.17–3.1.18): three or more words in capitals in a row without a patronymic and without a rank in front (на посаду МАСКУВАЛЬНИКОМ ІМІТАЦІЙНОГО ВІДДІЛЕННЯ ІНЖЕНЕРНО-МАСКУВАЛЬНОГО ВЗВОДУ, з посади СТАРШОГО ОПЕРАТОРА РОЗРАХУНКУ) stay as they are, and the real name next to them (… солдата ПЕТРЕНКА ОЛЕГА ПЕТРОВИЧА) is still masked. Word endings must fit the roles: a first name never ends in -ого/-ому (Імітаційного, Старшого) and ends in -их/-ів only if it is a known name (Яків); a surname in the instrumental (Начальником) needs a first name in the instrumental; words in -ння/-ття/-ість (Відділення, Відповідальність) are neither names nor surnames. Two words in capitals without a rank and without a patronymic are a name only when the second word is a known first name (КОВАЛЬ ТЕТЯНА yes, ПОМІЧНИКА ГРАНАТОМЕТНИКА and a rare ПЕТРЕНКО АЙДЕР no; Петренко Айдер and рядового ПЕТРЕНКА АЙДЕРА are masked). Known limitation: in ВРУЧИТИ ГРАМОТУ ОЛЕГУ ПЕТРОВИЧУ ПЕТРЕНКУ the first name and patronymic are masked, the surname after them is not — a word in capitals before a name-first order cannot be told from a surname.
Ranks are recognised next to a name (or in quotes as a standalone value); a rank word in running text (рядовий склад, Командир роти капітан) is left as it is — and masks never collide with it. Real output (3.1.x):
Rank declension (the surname keeps its case ending):
"молодшому сержанту Іванову …" → "старшому солдату Івенову …" (давальний)
"молодшого сержанта Іванова …" → "старшого солдата Івенова …" (родовий)
"молодшим сержантом Івановим …" → "старшим солдатом Івеновим …" (орудний)
First names and patronymics (3.1.12) follow the case of the original, and all case forms of one name share one mask — the mask is derived from the nominative:
"капітан Петренко Петро Іванович" → "лейтенант Пебоженко Павло Леонідович"
"капітана Петренка Петра Івановича" → "лейтенанта Пебоженка Павла Леонідовича"
"капітану Петренку Петру Івановичу" → "лейтенанту Пебоженку Павлу Леонідовичу"
"капітаном Петренком Петром Івановичем" → "лейтенантом Пебоженком Павлом Леонідовичем"
"сержанта Коваль Тетяни Сергіївни" → "старшого сержанта Ковар Катерини Павлівни"
"сержантом Коваль Тетяною Сергіївною" → "старшим сержантом Ковар Катериною Павлівною"
The gender and case of a first name are taken from the patronymic (Петра Івановича is a masculine genitive, not the feminine name Петра; Наталі Петрівни is genitive, Наталі Петрівні dative); without a patronymic — from the adjacent rank (рядового Петренка Богуслава → genitive of Богуслав). Forms in -ові (Петрові → Павлові), the vocative (Петре), the third declension (Любові → Лариси) and consonant alternations (Ользі → Ольга) are recognised. Unmasking does not depend on this: it restores by the mapping.
Women (the rank keeps its masculine form, as in documents; the first name and patronymic follow the gender):
"молодшим сержантом Коваленко Марією Петрівною" → "старшим солдатом Котвиненко Марусею Гордіївною"
"капітану Коваленко Марії Петрівні" → "лейтенанту Котвиненко Марусі Гордіївні"
Additional modifiers:
"Солдату у відставці Петренку …" → "Рядовому у відставці Пебоженку …"
"Сержанту в запасі Петренку …" → "Старшому сержанту в запасі Пебоженку …"
"Капітану на пенсії Петренку …" → "Лейтенанту на пенсії Пебоженку …"
A surname mask keeps up to the first 3 characters of the original, the rest is synthetic, and the grammatical ending is preserved. The prefix and the preserved ending together stay within half of the surname, so long endings shorten the prefix, but at least the first 2 letters are always kept: Коваль → Ковар, Іванов → Івенов, Ґудзь → Ґубко, Петренку → Пебоженку (the -енко ending is already half of the word, so only the minimum is kept). All letter-case and grammatical-case forms of one surname share one synthetic stem (МАЗУРЕНКА / Мазуренко → МАЛІЖЕНКА / Маліженко). Configure with masking_rules.surname_prefix_length (maximum, 0 = fully synthetic) and surname_prefix_min (minimum, default 2; 0 = the half rule alone, as before 3.1.4) and the faker dictionaries with system.faker_locale (default uk_UA; grammar stays Ukrainian).
"ІВАНОВ ІВАН ІВАНОВИЧ" → "ІВЕНОВ ІГОР ЛЕОНІДОВИЧ"
"Іванов Іван Іванович" → "Івенов Ігор Леонідович"
Names are recognised by their capital letter; an all-lowercase іванов іван is not a name for the parser and stays as it is.
The system generates identical mapping files when processing identical input data:
# Запуск 1:
"Іванов" → blake2b hash → seed → "Петренко"
# Запуск 2 (той самий файл):
"Іванов" → blake2b hash → seed → "Петренко" # Той самий результат!Advantages:
- One original = one mask (always)
- Results are predictable and reproducible
- You can re-run on the same file and get the same mapping
- Unmask works consistently
Technical details:
- Uses
hashlib.blake2b()for seed generation - Faker and random are initialized with a deterministic seed
Mapping files can be encrypted with AES-256-GCM:
# Маскування з шифруванням
python data_masking.py -i input.txt --encrypt --password-env DATA_MASKING_PASSWORD
# Результат: masking_map_*.enc — plaintext .json НЕ створюється (v3.0.1+)
# Той самий DATA_MASKING_PASSWORD читає і unmask_data.py
# Розмаскування з шифрованим mapping
python unmask_data.py masked.txt --map mapping.enc --password mypasswordPriority: CLI > ENV > config_local.yaml > config.yaml > config.py > Default. Keys left out of the file keep their defaults.
Shared configuration — config.yaml in the repository root (and next to the .exe in the Windows release) is read automatically when the program runs from that directory. It lists every key with its default and the full exclusion lists (dictionaries), so nothing has to be looked up in the code.
Private overrides — config_local.yaml. Keep shared settings in config.yaml and private ones (call signs, local abbreviations, your own paths) in config_local.yaml. It is looked up in the user config directory (~/.config/data-masking/, $XDG_CONFIG_HOME/data-masking/, %APPDATA%\data-masking\ on Windows) and next to config.yaml (or in the current directory); the one next to config.yaml is applied last. Every key set to a non-null value replaces the shared value (sections merge key by key, lists are replaced as a whole); null keeps the shared value. Start from config_local.example.yaml: cp config_local.example.yaml config_local.yaml. The file is in .gitignore; keep it chmod 600 (the program warns otherwise). --config-local FILE uses a specific file instead, --no-local-config ignores them; both work for data-mask and data-unmask.
data-mask --init-config # write a short config.yaml (the repository config.yaml lists every key)
data-mask -i input.txt --config config.yaml # ./config.yaml is also picked up automatically
data-mask -i input.txt --config docs/config-examples/share.yaml
data-unmask output.txt -c config.yaml
data-mask -i input.txt --config-local private.yaml # private overrides from a specific file
data-mask --list-exclusions # exclusions in effectAll keys with explanations: config_example.yaml.
Ready-made sets: docs/config-examples/
(share — keep dates and document numbers, strict — mask everything, fully synthetic surnames, pii — personal data only).
# Маскувати тільки ІПН та паспорти
python data_masking.py -i input.txt --only ipn,passport # кома або пробіл; невідомий тип — помилка (v3.0.1+)
# Маскувати все крім дат
python data_masking.py -i input.txt --exclude datesFor re-masking already masked files while preserving the full mapping chain:
from datamasking.extras.re_mask import ReMasker, MappingChain
chain = MappingChain()
remasker = ReMasker(chain=chain, mask_function=my_mask_fn)Atomic functions for integration into external applications:
from datamasking.extras.tools import mask_ipn_direct, mask_rank_direct, mask_pib_force
result = mask_ipn_direct("1234567890", masking_dict, instance_counters)from datamasking.extras.password_generator import generate_password
# Стандартний пароль (24 символи)
pwd = generate_password()
# З кирилицею
pwd = generate_password(length=32, include_cyrillic_lower=True)
# CLI
# python -m datamasking.extras.password_generator --length 32 --cyrillicStructured logging with JSON and colored console output:
from datamasking.extras.masking_logger import MaskingLogger, setup_logging
logger = MaskingLogger("masking")
setup_logging(level="DEBUG", format_type="json")Categories are switched in the YAML configuration (or for one run with --only / --exclude); no code changes are needed:
masking_rules:
enable_names: true # ПІБ (прізвища, імена, по батькові, ініціали)
enable_ipn: true # ІПН (10 цифр)
enable_passport: true # паспорти та ID-картки
enable_military_id: true # військові квитки
enable_ranks: true # звання
enable_units: true # військові частини
enable_brigades: true # бригади
enable_orders: true # номери наказів
enable_br_numbers: true # БР номери
enable_dates: true # дати
surname_prefix_length: 3 # літер оригінального прізвища в масці (0 = жодної)
surname_prefix_min: 2 # мінімум, навіть для «-енко» (0 = як до 3.1.4)
system:
hash_algorithm: "blake2b"
security:
encrypt_output: false # true = те саме, що --encryptSee config_example.yaml for every key and docs/config-examples/ for ready-made scenarios.
Hash algorithm comparison:
| Algorithm | Speed | Security | Usage |
|---|---|---|---|
| blake2b | ⚡⚡⚡⚡⚡ | ✅ High | Recommended (default) |
| md5 | ⚡⚡⚡⚡⚡ | Fastest, sufficient for seed | |
| sha1 | ⚡⚡⚡⚡ | Fast, better than md5 | |
| sha256 | ⚡⚡⚡ | ✅ High | Popular, Bitcoin |
| sha512 | ⚡⚡ | ✅ Maximum | Slowest |
Input:
Молодшому сержанту у відставці СИЧУ Роману Сергійовичу (Житомирський ОТЦКСП),
звільненому 31.05.2025, який є особою з інвалідністю III групи.
Result:
Старшому солдату у відставці БОЙКО Павлу Сергійовичу (Житомирський ОТЦКСП),
звільненому 15.06.2025, який є особою з інвалідністю III групи.
Input:
Капітанці медичної служби Коваленко Марії Іванівні, ІПН 1234567890,
ID-паспорт 123456789, видано наказом №75/25/3400/Р від 26.06.2025.
Result:
Майорці медичної служби Петренко Оксані Іванівні, ІПН 9876543210,
ID-паспорт 123947568, видано наказом №59/87/4249/Р від 10.07.2025.
Input:
Командир 72 окремої механізованої бригади
полковник Сергій ЗАПОРОЖЕЦЬ
Result:
Командир 85 окремої механізованої бригади
підполковник Павло ЗАВОДСЬКИЙ
Input:
Капітан ЗСУ Іванов Петро Миколайович, військовий квиток МТ123456,
проходив службу у МОУ, нагороджений СБУ за заслуги перед ГУР.
Result:
Майор ЗСУ Сидоров Андрій Миколайович, військовий квиток МТ654321,
проходив службу у МОУ, нагороджений СБУ за заслуги перед ГУР.
Input:
Сержанту БОНДАРЕНКО Олегу та молодшому сержанту КОВАЛЕНКО Андрію,
а також рядовому ШЕВЧЕНКО Петру видати по 100 грн.
Result:
Старшому сержанту БОСИЛЕНКО Омеляну та старшому солдату КОТВИНЕНКО Артему,
а також старшому солдату ШЕМАЧЕНКО Павлу видати по 100 грн.
{
"version": "3.1.20",
"mappings": {
"rank": {
"молодший сержант": {
"masked_as": "старший солдат",
"instances": [1, 3]
}
},
"surname": {
"Іванов": {
"masked_as": "Івенов",
"instances": [1, 2]
}
},
"passport_id": {
"123456789": {
"masked_as": "123947568",
"instances": [1]
}
}
},
"instance_tracking": {
"старший солдат": 3,
"Івенов": 2,
"123947568": 1
}
}Enlisted: рядовий, солдат, старший солдат, ефрейтор
Sergeants: молодший сержант, сержант, старший сержант, головний сержант, штаб-сержант, майстер-сержант, старший майстер-сержант, головний майстер-сержант
Officers: молодший лейтенант, лейтенант, старший лейтенант, капітан, майор, підполковник, полковник
Generals: бригадний генерал, генерал-майор, генерал-лейтенант, генерал
Sailors: матрос, старший матрос
Petty Officers: головний корабельний старшина, головний старшина, старшина 1/2 статті
Officers: молодший лейтенант, лейтенант, старший лейтенант, капітан-лейтенант
Captains: капітан 3/2/1 рангу
Admirals: контр-адмірал, віце-адмірал, адмірал
Medical: капітан/майор/підполковник/полковник медичної служби
Legal: капітан/майор/підполковник/полковник юстиції
- Називний (nominative): хто? що? — "солдат"
- Родовий (genitive): кого? чого? — "солдата"
- Давальний (dative): кому? чому? — "солдату"
- Орудний (instrumental): ким? чим? — "солдатом"
- Store mapping files in a secure location
- DO NOT transmit mapping files together with masked data
- Delete mapping files after work is complete
- Use different mappings for different documents
- Use mapping file encryption (v2.3.0+):
--encrypt --password - Unmask is ONLY possible with the mapping file
- Pass the password via
--password-env VAR_NAMEinstead of--password(command-line arguments are visible in shell history and process lists)
This is pseudonymization, not full anonymization. Consider the following:
- Partial preservation of the original. IPN retains the first 3 and last digit
(4 out of 10), passport retains 4 out of 9, surnames keep 2–3 leading letters and
the grammatical ending (
surname_prefix_length: 0removes the prefix). This is intentional (preserves format and partial context) but allows re-identification by brute-forcing against a candidate dictionary. - Deterministic generation without a secret. Masks are derived from the blake2b hash of the original without salt/key. Anyone who knows the algorithm and has a list of possible originals can match masks without the mapping file.
- Ranks are shifted by only ±1-2 positions in the hierarchy — the approximate rank of the person remains visible.
- Dates are shifted by ±30 days — the time period of events remains recognizable.
Conclusion: masked files protect against accidental disclosure and are suitable for sharing with a limited audience, but are NOT designed for publication against a motivated adversary with knowledge of the context.
- Check the mapping version (2.1 or any 3.x; 1.x mappings are restored with the v1 logic)
- Use the latest script versions
- Make sure
masking_rules.enable_ranks: trueand that the rank stands next to a name - Update to the latest version
- Update to v2.1.16+ with whitelist support
- Make sure
masking_rules.enable_passport: true - ID passports = 9 digits (not 10 like IPN)
# Add as submodule
git submodule add https://github.com/click0/data-masking.git
# Update submodule
git submodule update --remote data-masking
# Use in project
cd data-masking
python data_masking.py -i ../documents/input.txt# Clone separately
git clone https://github.com/click0/data-masking.git
cd data-masking
python data_masking.py -i input.txtBSD 3-Clause "New" or "Revised" License
Vladyslav V. Prodan
- GitHub: @click0
- Phone: +38(099)6053340
See CHANGELOG.md for version history and release notes.