Українська версія / Ukrainian version
Scripts for consistent masking of sensitive data in military documents with the ability to accurately restore the original.
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. 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 > YAML > 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_example.py— configuration example (dataclasses, no external dependencies)
pip install '.[full]' # from a repository checkout
data-mask 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 --verify input.txt recovered.txt # verify recovery- 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, masculine/feminine forms, with "retired"/"reserve" modifiers
- 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 (±30 days)
- Abbreviations — ЗСУ, МОУ, ВСУ, ДПСУ, НГУ, ДСНС, СБУ, ГУР, ТЦК, СП
Each occurrence is tracked individually:
Вхід: "Іванов зустрів Петрова, потім Іванов пішов"
Маска: "Сидоров зустрів Коваля, потім Сидоров пішов"
↑ instance 1 ↑ instance 2
Unmask правильно відновить обидва входження
Rank declension:
"молодшому сержанту" → "старшому солдату" (давальний)
"молодшого сержанта" → "старшого солдата" (родовий)
"молодшим сержантом" → "старшим солдатом" (орудний)
Name declension:
"Іванову Петру" → "Сидорову Андрію" (давальний)
"Іванова Петра" → "Сидорова Андрія" (родовий)
"Івановим Петром" → "Сидоровим Андрієм" (орудний)
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 never exceed half of the surname, so long endings shorten the prefix: Коваль → Ковар, Іванов → Іщенов, Ґудзь → Ґубко, Петренку → Єрченку (the -енко ending is already half of the word). All letter-case and grammatical-case forms of one surname share one synthetic stem (МАЗУРЕНКА / Мазуренко → ТЕЛІЖЕНКА / Теліженко). Configure with masking_rules.surname_prefix_length (0 = fully synthetic) and the faker dictionaries with system.faker_locale (default uk_UA; grammar stays Ukrainian).
"ІВАНОВ" → "ПЕТРЕНКО"
"Іванов" → "Петренко"
"іванов" → "петренко"
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.yaml > Default
# Використання YAML конфігурації
python data_masking.py -i input.txt -c config.yaml
# Генерація прикладу конфігурації — див. config_example.py# Маскувати тільки ІПН та паспорти
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", json_output=True)In data_masking.py you can enable/disable categories:
# Алгоритм хешування для детермінованої генерації
HASH_ALGORITHM = 'blake2b' # blake2b (рекомендовано), md5 (швидкий), sha256 (популярний)
MASK_IPN = True # ІПН (10 цифр)
MASK_PASSPORT = True # Паспорти (АА123456) та ID-паспорти (9 цифр)
MASK_MILITARY_ID = True # Військові квитки
MASK_NAMES = True # Імена та прізвища
MASK_RANKS = True # Звання
MASK_BRIGADES = True # Бригади
MASK_MILITARY_UNITS = True # Військові частини
MASK_ORDER_NUMBERS = True # Номери наказів
MASK_BR_NUMBERS = True # БР номери
MASK_DATES = True # Дати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": "2.0",
"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, long surnames retain the first 3 and last 5 characters. 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 (should be v2.0)
- Use the latest script versions
- Make sure
MASK_RANKS = True - Update to the latest version
- Update to v2.1.16+ with whitelist support
- Make sure
MASK_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.