Разговор о защите персональных данных часто сводится к выбору «инструмента маскирования», но у этого выбора есть неочевидная механика: разные способы замены по-разному влияют на связность записей, обратимость и аналитическую ценность поля. Ниже — разбор того, как устроены операторы деидентификацииобъекты Presidio, выполняющие операцию анонимизации над сущностью PII, как движок выбирает их для каждой сущности и что из этого следует для корпоративного конвейера.
Деидентификация и её частные случаи
деидентификацияпроцесс удаления или замены идентифицирующих сведений так, чтобы связать запись с человеком стало затруднительно реализуется через операторобъект, выполняющий операцию анонимизации над сущностью PII. Встроенный набор включает замену, полное удаление, хеширование, маскирование, шифрование, пользовательский оператор, сохранение без изменений, а также обратный оператор расшифровкиоператор, восстанавливающий зашифрованный PII тем же ключом, которым он был зашифрован.
Разница между ними — в том, что происходит с исходным значением. Замена подставляет заданное значение, а если оно не задано или пусто, по умолчанию используется тип сущности. Полное удаление убирает PII из текста. Маскирование заменяет заданное число символов указанным символом. Сохранение оставляет PII без изменений. Хеширование преобразует текст с сольюдополнительное значение, добавляемое к тексту перед хешированием, чтобы одинаковые исходные данные давали разные хеши. Шифрование шифрует текст заданным ключом, и поскольку операция обратима, расшифровка восстанавливает исходное значение тем же ключом.
Связность данных сохраняют только хеширование при согласованной соли и пара шифрование/расшифровка при одном ключе. Замена, удаление, маскирование и сохранение восстановления исходного значения не обеспечивают.
Почему маскирование разрушает аналитику, а хеш и шифрование — нет
Маскирование заменяет указанное число символов одним и тем же символом, поэтому все замаскированные значения становятся одинаковыми по форме и теряют различия между исходными сущностями. Именно это уничтожает вариативность поля: по замаскированному значению нельзя отличить одну запись от другой.
Хеширование ведёт себя иначе. При отсутствии соли генерируется случайная соль для каждой сущности, а перед хешированием текст и соль объединяются. Поэтому одинаковые исходные значения дают разные результаты, и поле сохраняет различающую способность — но уже не относительно исходных значений, а как новый набор уникальных меток.
Шифрование сохраняет возможность восстановления через расшифровку, но не раскрывает исходные данные. Разница в итоге такая: маскирование уничтожает вариативность, а хеширование и шифрование создают новые значения, не совпадающие с исходными, но различающиеся между собой.
Как пошагово работает маскирование
Сначала вычисляется фактическое число символов для замены: если запрошенное количество положительно, берётся минимум из длины текста и запрошенного значения, иначе — ноль. То есть запрошенное число не может превысить длину текста и не может быть отрицательным.
return min(len(text), chars_to_mask) if chars_to_mask > 0 else 0Направление маскирования управляется параметром «с конца»: когда он выключен, маска ставится в начале строки и заменяются первые символы, а остаток исходного текста сохраняется; когда он включён, индекс начала маски вычисляется как разность длины текста и числа маскируемых символов, и маскируется хвост строки. От этого выбора зависит, какая часть значения остаётся видимой: при маскировании с начала открытым остаётся конец строки, при маскировании с конца — её начало. В обеих ветвях длина результата равна длине исходного текста: заменяется ровно столько символов, сколько было запрошено, на такое же количество символов замены, а остальные берутся из исходного текста без изменений.
Параметры маскирования и их валидация
Оператор принимает три параметра: символ замены, количество символов и признак маскировки с конца. При валидации сначала проверяется, что символ замены — строка, затем что его длина не больше одного символа, иначе выбрасывается ошибка недопустимого параметра. Далее проверяется, что количество символов — целое, а признак — булево.
Значений по умолчанию при валидации нет: параметры извлекаются без подстановки дефолтов. Если символ замены окажется строкой длиной больше одного символа, валидация выбросит ошибку с сообщением, что параметр должен быть символом.
Поведение при выходе за границы
Если запрошенное число символов превышает длину текста, фактическое число ограничивается длиной текста — маскируется весь текст. Если запрошенное число меньше или равно нулю, возвращается ноль, и маскирование не производится.
При маскировании с начала символ замены повторяется фактическое число раз и склеивается с остатком текста начиная с соответствующего индекса. При маскировании с конца индекс начала маски вычисляется как длина текста минус число маскируемых символов, и результат — текст до этого индекса плюс повторённый символ замены.
Как устроено хеширование и почему важна соль
Хеширование преобразует текст PII с солью, используя sha256криптографическая хеш-функция, выдающая 256-битное значение или sha512криптографическая хеш-функция, выдающая 512-битное значение. Соль можно передать явно: если параметр присутствует, берётся пользовательская соль, строку кодируют в байты и проверяют, что она не пустая и не короче 16 байт. Иначе генерируется случайная соль размером 32 байта. Перед хешированием текст кодируется и конкатенируется с солью, после чего выбирается алгоритм и возвращается hexdigestшестнадцатеричная строка — результат хеширования.
Поскольку при отсутствии соли соль случайна для каждой сущности, одно и то же значение PII получает разные хеши в разных сущностях и вызовах. Это предотвращает brute-force и словарные атаки, но нарушает referential integrityсвойство, при котором одинаковые исходные значения дают одинаковый результат и записи можно связать между собой. Чтобы одинаковые значения давали одинаковый хеш, нужно явно передавать согласованную соль. Presidioбиблиотека для обнаружения и анонимизации персональных данных не хранит состояние сессий, поэтому пользователь должен сам безопасно хранить и переиспользовать соль. В примере соль генерируется один раз и передаётся в конфигурацию оператора, чтобы оба вхождения одного и того же имени получили одинаковый хеш.
Как работает шифрование и чем оно отличается от хеша
Шифрование принимает ключ, который может быть как байтами, так и строкой: если это строка, она кодируется в UTF-8 перед использованием. Валидация проверяет, что ключ имеет длину 128, 192 или 256 бит, иначе выбрасывается ошибка недопустимого параметра.
Шифрование обратимо: результат можно восстановить оператором расшифровки, который использует тот же ключ. Хеширование, напротив, необратимо — оно возвращает hexdigest от текста с солью, и восстановить исходный текст нельзя. Для сохранения связности в хеширование можно передать постоянную соль, тогда одинаковые входные данные дают одинаковый хеш; если соль не указана, генерируется случайная соль на каждую сущность, что нарушает связность между вызовами.
Сдвиг значений: как замена выполняется с конца
Оператор замены использует построитель текста, который хранит исходный текст, текущий выходной текст и индекс последней замены, изначально равный длине текста. При замене вычисляется конечный индекс как минимум из конца заменяемого фрагмента и индекса последней замены, после чего индекс последней замены становится равен началу фрагмента. Новый выходной текст собирается как текст до начала, текст замены и текст после конечного индекса. Метод возвращает позицию вставки, отсчитанную от конца.
Алгоритм заменяет текст с конца к началу, поэтому уже обработанные сущности, находящиеся правее, не сдвигаются: их позиции остаются валидными относительно исходного текста.
Валидация позиции при замене
Метод проверки позиции сравнивает сохранённую длину исходной строки с границами фрагмента и, если начало больше длины текста или конец выходит за неё, формирует сообщение об ошибке и выбрасывает исключение недопустимого параметра. Этот метод вызывается перед возвратом среза исходного текста. При попытке обратиться к позиции за пределами длины исходной строки будет выброшено исключение с текстом, содержащим начало, конец и фактическую длину текста. Сам метод замены с вычислением позиции вставки эту проверку не вызывает, а вместо этого ограничивает конечную позицию через минимум из конца фрагмента и индекса последней замены.
Слияние соседних сущностей одного типа
Функция слияния проходит по списку результатов, сохраняя предыдущий результат. Если у текущего и предыдущего результата совпадает тип сущности и текст между ними состоит только из пробелов, то предыдущий результат удаляется, а текущему присваивается его начало. Тем самым две соседние сущности одного типа, разделённые только пробелами, объединяются в одну с общим началом и концом. Без этого шага имя и фамилия остались бы двумя отдельными результатами распознавания, и при анонимизации каждая часть получила бы свою отдельную маску.
Разрешение конфликтов между распознавателями
Конфликты разрешаются в два этапа. Сначала строится список прочих элементов из копии результатов, и для каждого результата проверяется, конфликтует ли он хотя бы с одним другим элементом. Неконфликтующие результаты попадают в итоговый набор, конфликтующие отбрасываются. При удалении пересеченийстратегия разрешения конфликтов, при которой пересекающиеся сущности обрезаются по границам друг друга, а не отбрасываются целиком элементы дополнительно сортируются по началу, и для пересекающихся пар сравниваются оценкичисловая оценка уверенности распознавателя в том, что найденный фрагмент действительно является сущностью PII: если оценка текущей сущности не ниже оценки следующей, начало следующей сдвигается к концу текущей, иначе конец текущей сдвигается к началу следующей. После корректировки элементы, у которых начало оказалось правее конца, удаляются.
По документации при полном пересечении выбирается сущность с более высоким scoreчисловая оценка уверенности распознавателя в том, что найденный фрагмент действительно является сущностью PII, при одинаковых score выбор произвольный; при вложенности выбирается сущность с большим текстом, даже если её score ниже; при частичном пересечении каждая сущность анонимизируется отдельно и возвращается конкатенация анонимизированного текста.
Что происходит при полном перекрытии с разными оценками
При полном перекрытии сущностей с разными типами и разными оценками побеждает сущность с более высокой оценкой. Например, при пересечении телефонного номера с оценкой 0.7 и номера социального страхования с оценкой 0.8 будет выбрана вторая, поскольку её оценка выше. Если оценки одинаковые, выбор произвольный. В коде это реализовано так: результат отбрасывается, если он конфликтует с другим элементом, а проверка конфликта выполняется через сравнение с каждым из прочих элементов.
Как фабрика загружает и находит операторы
При создании экземпляра фабрики предопределённые операторы загружаются для каждого типа: для анонимизации берётся список ANONYMIZERSнабор классов операторов анонимизации: Custom, Encrypt, Hash, Keep, Mask, Redact, Replace и, при доступности, AHDSSurrogate, а для деанонимизации — DEANONYMIZERSнабор классов операторов деанонимизации: Decrypt и DeanonymizeKeep. Эти два списка различаются по составу: в анонимизации доступны операторы, изменяющие или скрывающие PII, а в деанонимизации — только операторы, восстанавливающие исходные значения (Decrypt) либо сохраняющие их без изменений (DeanonymizeKeep). Из каждого класса создаётся экземпляр, у которого запрашивается строковое имя. Результатом является словарь «имя — класс». Например, маскирование возвращает имя "mask", шифрование — "encrypt", хеширование — "hash".
При поиске класса по имени сначала запрашивается словарь всех классов по типу, затем ищется класс по имени. Если тип не найден, возбуждается ошибка недопустимого параметра с сообщением о неверном типе оператора; если имя не найдено — с сообщением о неверном классе оператора. При успехе возвращается экземпляр найденного класса. Таким образом, попытка использовать незарегистрированный оператор приводит к исключению, а не к молчаливому пропуску.
Связывание типа сущности с оператором
Регистрация оператора принимает класс и кладёт его в фабрику под ключом, который возвращает сам оператор. При проверке словаря операторов, если он пуст, возвращается словарь с ключом по умолчанию и значением конфигурации оператора заменыоператор, который подставляет вместо найденной сущности заданное значение, а если оно не задано — тип сущности; если словарь непустой, но в нём нет ключа по умолчанию, он добавляется. Если для сущности не задан явный оператор, движок подставляет вместо неё заданное значение, а при отсутствии заданного значения — тип сущности.
Результаты анализатора копируются: создаются новые объекты с теми же началом, концом, типом сущности и оценкой, чтобы исходный объект не был изменён, поскольку далее список сортируется и модифицируется.
Производительность и компромиссы
Сравнение стоимости обработки изображений и вызова внешнего сервиса суррогатизации с локальными операторами маскирования, хеширования и шифрования невозможно: утверждений о том, что эти шаги дороже, нет. Оператор суррогатов описан как генерация реалистичных медицинских замен через сервис Azure Health Data ServicesОблачная служба Microsoft, чей сервис деидентификации используется оператором surrogate_ahds для генерации реалистичных медицинских суррогатов., для него указаны endpointадрес внешнего сервиса, к которому обращается оператор, список сущностей, входная и выходная локали, а также требование установки дополнительного пакета.
Обратимость заложена только в паре шифрование/расшифровка: шифрование преобразует текст заданным ключом, а расшифровка восстанавливает его тем же ключом. Хеширование необратимо и по умолчанию использует случайную соль, поэтому одинаковые значения дают разные хеши, что защищает от brute-force и словарных атак, но ломает связность; для одинаковых хешей нужно явно передать соль не короче 16 байт. Маскирование заменяет заданное число символов указанным символом, сохраняя остальной текст, а шифрование сохраняет возможность восстановления исходного значения при наличии ключа.
Что из этого следует на практике
- Маскирование годится для отображения, но не для аналитики. Оно уничтожает различия между записями, поэтому по замаскированному полю нельзя строить статистику или связывать записи.
- Хеш сохраняет связность только при явной согласованной соли. Без неё одинаковые значения дают разные хеши, и связать записи между вызовами не получится. Соль нужно хранить и переиспользовать самому — движок состояние сессий не ведёт.
- Шифрование — единственный способ вернуть исходное значение. Обратимость есть только в паре шифрование/расшифровка при одном ключе; хеш восстановить нельзя.
- Маскирование не выходит за границы текста. Запрошенное число символов ограничивается длиной строки, а при нулевом или отрицательном значении маскирование не производится вовсе.
- Соседние сущности одного типа, разделённые пробелами, объединяются. Иначе каждая часть получила бы отдельную маску.
- При конфликте распознавателей побеждает более высокая оценка, а при вложенности — более длинный фрагмент. Это определяет, какая сущность в итоге будет замаскирована.
- Незарегистрированный оператор — это исключение, а не тихий пропуск. Опечатка в имени оператора остановит обработку, а не оставит PII в открытом виде.
Где смотреть в коде
- mask.py: Mask
- hash.py: Hash
- text_replace_builder.py: __init__
- anonymizer_engine.py: _merge_entities_with_spaces_between
- anonymizer_engine.py: get_anonymizers
- operators_factory.py: add_deanonymize_operator
- operators_factory.py: import
- anonymizer_engine.py: add_anonymizer