[Перевод] Осторожнее с копипастом: фингерпринтинг текста непечатаемыми символами
Не хотите читать? Посмотрите демо.
Символы нулевой ширины — это непечатаемые управляющие символы, которые не отображаются большинством приложений. Например, в это предложение я вставил десять пробелов нулевой ширины, вы это заметили? (Подсказка: вставьте предложение в Diff Checker, чтобы увидеть местоположение символов!). Эти символы можно использовать как уникальные «отпечатки» текста для идентификации пользователей.
Безусловно, он может здесь быть. И вы никогда не догадаетесь
Зачем?
Ну, изначальная причина не слишком интересна. Несколько лет назад я с командой участвовали в соревнованиях по различным видеоиграм. У команды была приватная страничка для важных объявлений, среди прочего. Но в итоге эти объявления стали репостить в других местах, с издевательствами над командой, раскрывая конфиденциальную информацию и командную тактику.
Защита сайта казалась довольно стойкой, поэтому мы выдвинули предположение, что действует инсайдер, который входит по логину и паролю, а потом просто копирует объявление и размещает в другом месте. Поэтому я разработал скрипт, который в каждом объявлении невидимо отпечатывает имя пользователя, которому отображается это объявление.
После недавнего поста Зака Айсана стало понятно, что людям интересна тема непечатаемых символов. Так что я решил опубликовать этот метод здесь вместе с интерактивной демонстрацией для всех. Примеры кода обновлены для современного JavaScript, но общая логика одинакова.
Как?
Точные шаги и логика описаны ниже, но если в двух словах: строка имени пользователя преобразуется в двоичную форму, затем двоичный файл преобразуется в серию непечатаемых символов, представляющих каждый бит. Потом непечатаемая строка незаметно вставляется в текст. Если текст опубликован на другом сайте, строку непечатаемых символов можно извлечь и провести обратный процесс, чтобы выяснить имя пользователя, который сделал копипаст!
Фингерпринтинг текста
1. Получить имя пользователя, вошедшего в систему, и преобразовать его в двоичный файл.
Здесь мы просто преобразуем каждую букву имени пользователя в двоичный эквивалент.
const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
username.split('').map(char =>
zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);
2. Взять имя пользователя в бинарном формате и преобразовать его в непечатаемые символы
Следующий скрипт перебирает двоичную строку и преобразует каждый бит 1 в непечатаемый пробел, каждый 0 — в непечатаемый символ запрета лигатур (non-joiner). После преобразования каждой буквы вставляем непечатаемый символ разрешения лигатур (joiner) — и переходим к следующей.
const binaryToZeroWidth = binary => (
binary.split('').map((binaryNum) => {
const num = parseInt(binaryNum, 10);
if (num === 1) {
return ''; // zero-width space
} else if (num === 0) {
return ''; // zero-width non-joiner
}
return ''; // zero-width joiner
}).join('') // zero-width no-break space
);
3. Вставка «имени пользователя» в непечатаемый конфиденциальный текст
Здесь просто вставляем блок непечатаемых символов в конфиденциальный текст.
Извлечение имени пользователя из помеченного текста
Те же действия в обратном порядке.
1. Извлечь непечатаемое «имя пользователя» из конфиденциального текста
Удалить конфиденциальный текст из строки, оставив только непечатаемые символы.
2. Преобразовать непечатаемое «имя пользователя» обратно в двоичный файл
Здесь мы разбиваем строку на фрагменты, с учётом добавленных межбуквенных разделителей. Это даёт эквивалент в управляющих символах для каждой буквы имени пользователя! Перебираем символы и возвращаем 1 или 0, чтобы воссоздать двоичную строку. Если не находим соответствующий 1 или 0, то значит попали на межбуквенный разделитель (символ разрешения лигатур) и, таким образом, завершиили двоичное преобразование для символа: можно добавить к строке один пробел и переходить к следующему символу.
const zeroWidthToBinary = string => (
string.split('').map((char) => { // zero-width no-break space
if (char === '') { // zero-width space
return '1';
} else if (char === '') { // zero-width non-joiner
return '0';
}
return ' '; // add single space
}).join('')
);
3. Преобразование имени пользователя из двоичного формата обратно в текст
В конце концов, анализируем двоичную строку и преобразуем каждую серию 1 и 0 в соответствующий символ.
const binaryToText = string => (
string.split(' ').map(num =>
String.fromCharCode(parseInt(num, 2))).join('')
);
Заключение
Компании как никогда много внимания уделяют утечкам информации и поиску инсайдеров. Этот лишь один из многих трюков, которые можно использовать. В зависимости от направления вашей работы, может быть жизненно важно понимать риски, связанные с копированием текста. Очень немногие приложения отображают непечатаемые символы. Например, вы можете предположить, что ваш терминал попытается их отобразить (мой нет!).
Если вернуться к секретной доске объявлений, то план сработал как надо. Вскоре после внедрения скрипта вышло новое объявление. В течение нескольких часов текст распространили в другом месте с прикрепленной непечатаемой строкой. Имя пользователя виновника успешно идентифицировали, и его забанили: хэппи-энд!
Конечно, есть определённые оговорки по использованию этого метода. Например, если пользователь знает о скрипте, то теоретически может заменить непечатаемые символы, чтобы подставить другого человека. Так что лучше вместо имени пользователя вставлять уникальный секретный ID.
Чтобы поиграться со скриптом, запускайте демо или смотрите исходный код.