Разбор, на который меня зовут регулярно, и заканчивается он почти всегда одинаково. База клиентов всплывает в продаже. Выгрузку за последний год получали десятки контрагентов — интеграторы, колл-центр, маркетинговое агентство, аудиторы. У всех был законный доступ. Файл у всех был один и тот же.

Дальше начинается разговор в жанре «это не мы», и закончить его нечем: копии побайтово одинаковые, доказать причастность нельзя ни к кому.

Чинится это не после инцидента, а до него — и стоит недорого. Каждый получатель должен получать копию, отличающуюся от остальных так, чтобы отличие не мешало работе и переживало пересохранение файла.

Сколько информации нужно спрятать

Задача формулируется как передача идентификатора получателя внутри самих данных. Сорок подрядчиков — это шесть бит:

40 получателей   -> нужно 6 бит
500 получателей  -> нужно 9 бит
5000 получателей -> нужно 13 бит

Числа скромные, и это главная хорошая новость: прятать нужно не «метку», а полдюжины бит. Места под них в любой реальной выгрузке избыточно много.

Где брать биты, не искажая данные

Ключевое требование — данные должны остаться верными. Подрядчик работает с настоящими клиентами, и подменять им телефоны нельзя. Значит, использовать надо ту свободу, которая в формате уже есть и на смысл не влияет.

Порядок строк. Самый ёмкий источник. Перестановки n элементов дают log2(n!) бит:

10 строк   -> 21.8 бит
20 строк   -> 61.1 бит
50 строк   -> 214.2 бит
1000 строк -> 8529.4 бит

Двадцать строк дают 20! ≈ 2,4·10¹⁸ перестановок — на этом уровне вопрос ёмкости просто не стоит. В выгрузке на сто тысяч записей её некуда девать.

Механика — детерминированная сортировка по функции от идентификатора получателя. Хранить сами копии не нужно, достаточно ключа и списка выданных идентификаторов:

import hmac, hashlib

KEY = load_key()   # из секрет-хранилища, не из репозитория

def mark(rows, recipient_id):
    def rank(row):
        return hmac.new(KEY, f"{recipient_id}|{row['id']}".encode(),
                        hashlib.sha256).digest()
    return sorted(rows, key=rank)

def identify(leaked_rows, candidates):
    order = [r["id"] for r in leaked_rows]
    for cid in candidates:
        if [r["id"] for r in mark(leaked_rows, cid)] == order:
            return cid
    return None
порядок в выданной копии: [7, 6, 2, 4, 3, 5, 1, 8]
порядок для другого получателя: [6, 4, 5, 7, 8, 3, 1, 2]
утекла копия -> podryadchik-17

Восемь строк дают 8! = 40 320 вариантов порядка — с большим запасом на сорок подрядчиков, и опознание сводится к перебору сорока кандидатов.

HMAC взят не для красоты. Свой порядок строк получатель, разумеется, видит — метка лежит у него перед глазами. Ключ нужен для другого: без него нельзя ни построить копию, которая укажет на другого подрядчика, ни выдать свою за чужую.

Оговорка к этому коду: он требует точного совпадения всей перестановки. На практике из утёкшего файла что-то удалено или в него что-то дописано, и сравнивать надо не порядок целиком, а долю пар строк, стоящих в том же относительном порядке, что и в копии кандидата. Тогда запас в шестьдесят бит превращается в устойчивость к частичной порче, а не просто в лишние знаки.

Незначащие вариации формата. Телефон записывается как +7 (495) 000-00-00 или +74950000000, дата — как 2026-08-14 или 14.08.2026, регистр домена в адресе почты роли не играет. Каждое такое решение — как минимум один бит на поле, а если вариантов написания больше двух, то и больше. Способ менее ёмкий, чем перестановка, зато переживает сортировку строк.

Фантомные записи. В выгрузку добавляется несколько несуществующих клиентов с контактами, которые контролируете вы: отдельный номер, отдельный почтовый ящик, отдельный адрес. У каждого получателя набор фантомов свой.

Это самый грубый метод и одновременно самый полезный. Он переживает любую обработку данных: фильтрацию, сортировку, дедупликацию, выгрузку в другой формат, ручное копирование в чужую CRM. И он даёт не расчёт, а событие — звонок или письмо на контакт, который существует ровно в одной копии.

Что переживает обработку, а что нет

Метка бесполезна, если умирает при первом сохранении. Практический расклад такой.

Порядок строк ломается сортировкой. Достаточно одного клика по заголовку столбца в Excel — и метки нет. Причём ломается он и без злого умысла, просто в ходе работы.

Вариации формата ломаются нормализацией. Любой импорт в приличную CRM приведёт телефоны к одному виду.

Фантомные записи обработкой не ломаются вовсе — их можно только заметить и вычистить, а для этого нужна сверка с другим источником. Обычно их находят уже после того, как метка сработала.

Отсюда правило, которое стоит закладывать сразу: методы комбинируются. Порядок строк даёт много бит и работает, пока файл не трогали. Фантомы дают мало бит, но доживают до конца. Вместе они закрывают и аккуратную утечку файла целиком, и утечку переработанной выгрузки.

Что будет, если получатели сравнят свои копии

Слабое место всех подобных схем: двое получателей могут сравнить свои копии. Различия видны сразу, и дальше можно собрать третью копию, не совпадающую ни с одной из исходных, — или просто вычистить всё, что различается.

Задача эта изучена, и решения известны: коды, устойчивые к сговору, — схема Бонэ–Шоу и коды Тардоса. Идея в том, что метка кодируется избыточно и вероятностно, поэтому по «усреднённой» копии всё равно вычисляется хотя бы один из участников сговора, а вероятность обвинить непричастного ограничена сверху заданной величиной. Плата — длина метки. У кодов Тардоса она растёт как квадрат числа сговорившихся, умноженный на логарифм числа получателей, и это оптимальный порядок. У более ранней схемы Бонэ–Шоу расход заметно выше, она интересна скорее как первая конструкция с доказанной стойкостью.

Честная оценка: для типового случая с десятками подрядчиков это перебор. Смысл появляется там, где получателей тысячи, а данные дорогие. Знать про эти коды стоит хотя бы затем, чтобы не изобретать защиту от сговора самостоятельно — самодельные схемы ломаются сравнением двух копий.

Юридические и этические границы

Здесь легко сделать хуже, чем было.

Фантомные записи должны содержать только ваши собственные контакты. Вымышленный клиент со служебным номером компании персональными данными не является — а вот вписанный «для правдоподобия» настоящий чужой телефон является, и обрабатывать его вам никто не разрешал.

Получателей о маркировке лучше уведомлять — пунктом в договоре о том, что выгрузка индивидуально маркирована. Это снимает разговор о скрытом изменении данных, а заодно работает лучше любой метки: знание о том, что копия именная, останавливает ровно ту категорию утечек, против которой метка и задумана, — вынос данных своими руками.

Метка доказывает, из какой копии данные, а не кто именно их вынес. Копия могла утечь у подрядчика из-за взлома или неаккуратности. Вывод «нашли виновного» из результата не следует — следует «нашли, где искать».

Что сделать до следующей выгрузки

Завести реестр выгрузок: кому, когда, какой состав полей, какой идентификатор получателя. Без реестра любая метка бессмысленна, потому что сравнивать будет не с чем. По моему опыту, именно этого пункта не хватает чаще всего — маркировку внедрить успевают, а список того, что кому уходило, ведут в переписке.

Встроить маркировку в тот код, который формирует выгрузку, а не делать её руками. Ручная маркировка не переживает отпуск сотрудника.

Добавить фантомные контакты и завести правило, что звонки и письма на них попадают в мониторинг как события. Сработавший фантом — это готовое начало расследования с точной привязкой к получателю и датой.

И проверить, что ключ маркировки лежит отдельно от выгрузок и от их реестра. Ключ в том же репозитории, где скрипт выгрузки, превращает всю схему в украшение.

Комментарии (0)