Защита, которая оборачивается против вас
Фингерпринтинг canvas работает потому, что растеризация на GPU не стандартизирована на уровне пикселей. Попросите две машины нарисовать один и тот же текст тем же шрифтом того же размера — и сглаживание будет отличаться на волосок. Возьмите хеш пикселей и получите значение, стабильное на одном устройстве и другое на следующем.
Интуитивная контрмера — добавить шум: изменить несколько пикселей до вычисления хеша, чтобы значение менялось и не годилось для слежки. Такую опцию предлагает почти каждый антидетект-браузер, обычно под названием «canvas: noise».
Загвоздка в том, что у настоящего браузера хеш canvas не меняется. Та же машина, тот же браузер, та же страница — тот же результат: сегодня, завтра и после перезагрузки. Именно эта стабильность и делает сигнал ценным для сбора.
Значит, значение, меняющееся при каждом чтении, не стало анонимным. Оно стало аномальным. Это ошибка того же класса, что и выставление navigator.webdriver = false: честный ответ вас опознаёт, а нечестный опознаёт как того, кто лжёт.
Как на самом деле работает обнаружение
Чтобы заметить рандомизацию, не нужно ничего хитрого. Четыре способа, примерно в порядке частоты встречаемости:
Нарисовать одно и то же дважды
Дважды отрисовать одинаковый canvas в пределах одной загрузки страницы и взять оба хеша. Настоящий браузер: одинаковые. Шум на каждый вызов: разные. Это две строки кода, и они есть в любом серьёзном скрипте обнаружения.
Усреднить шум
Если шум случайный и с нулевым средним, то n-кратная отрисовка с усреднением подтягивает результат к истинному значению со скоростью около √n. Двадцати отрисовок обычно хватает, чтобы восстановить исходный хеш. Рандомизация стоила трекеру нескольких миллисекунд, а вам — анонимности, да ещё и пометила вас как того, кто рандомизирует.
Проверить, уместен ли шум
Рандомизированный canvas сам по себе не подозрителен. Brave включает его по умолчанию — его «farbling» выводит шум из сида, своего для сессии и источника. Tor Browser вместо этого спрашивает пользователя. Важно другое: соответствует ли шум браузеру, за который вы себя выдаёте. Chrome canvas не рандомизирует. Клиент, у которого user agent сообщает Chrome 147, а canvas меняется на каждом вызове, сам себе противоречит — и находкой становится противоречие, а не шум.
Менять путь отрисовки
Наивные реализации перехватывают toDataURL() и getImageData(). Есть и другие выходы: toBlob(), createImageBitmap(), readPixels() в WebGL, OffscreenCanvas внутри worker. Если на одном пути шум есть, а на другом нет, они расходятся — и это расхождение весомее любого из значений по отдельности.
Отдельно о пути через worker
Последний пункт заслуживает отдельного упоминания, потому что именно на нём ломается большинство реализаций.
OffscreenCanvas можно передать в Web Worker и отрисовать там. У worker свой глобальный контекст и своя копия canvas-API, и внедрение скрипта на уровне страницы туда не достаёт. Если ваш шум накладывается внедрённым JavaScript, canvas из worker выйдет чистым.
const off = new OffscreenCanvas(280, 60);
const w = new Worker(url);
w.postMessage({ canvas: off }, [off]);
// the worker draws and hashes — untouched by any page-level hook
Теперь у страницы два хеша canvas с одного устройства, которые не совпадают. Законной конфигурации, в которой такое возможно, не существует. Это та же структурная проблема, что описана в статье про CDP: всё, что применяется изнутри страницы, не дотягивается до контекста, который странице не принадлежит.
Как выглядит корректная реализация
Цель не в том, чтобы «каждый раз было другое значение». Цель — значение, отличное от чужих, и каждый раз одно и то же для меня. В такой формулировке требования вытекают сами:
| Свойство | Требование |
|---|---|
| В пределах одной загрузки страницы | Одинаково при каждой отрисовке |
| Между сессиями | Одинаково для одного профиля, всегда |
| Между профилями | Различается, и не на постоянную величину |
| Между путями отрисовки | 2D, WebGL, OffscreenCanvas и worker согласуются |
| Правдоподобие | Соответствует видеокарте, заявленной профилем |
Механизм, удовлетворяющий всем пяти условиям, — это сид. Зафиксируйте случайный сид при создании профиля, сохраните его и детерминированно выводите из него изменение пикселей. Любая отрисовка этого профиля даст одинаковый результат, потому что сид один и тот же. Два профиля различаются, потому что различаются их сиды. А поскольку вывод происходит там, где пиксели и появляются — в растеризаторе, а не в перехваченном методе JavaScript, — одинаковую обработку получает каждый путь, включая worker.
Именно последний пункт объясняет, почему это трудно сделать как следует без изменённого движка. Нет поддерживаемого способа дотянуться из контент-скрипта до растеризатора внутри worker.
Доводы за то, чтобы ничего не трогать
Есть серьёзный довод, который стоит изложить честно: canvas можно вообще не трогать.
Если у вас один профиль на одну физическую машину, ваш подлинный хеш canvas — совершенно обычное значение совершенно обычного компьютера. Он стабилен, согласован по всем путям и соответствует вашей видеокарте, потому что он и есть ваша видеокарта. Ничто в нём не заставляет присмотреться.
Работать это перестаёт в тот момент, когда вы запускаете на той же машине второй профиль. Теперь оба профиля дают одинаковый хеш, и любая площадка, снимающая отпечаток canvas, может их связать — то есть ровно то, чего вы и пытались избежать.
Значит, выбор не между «шум или не шум». Он такой:
- Одна личность на машину — canvas не трогайте. Настоящее железо, настоящий хеш, объяснять нечего.
- Несколько личностей на машину — нужны значения canvas для каждого профиля, и они должны быть стабильными и согласованными. Рандомизация на каждый вызов — худший из трёх вариантов: она и не прячет вас, и не мешает связыванию.
Проверьте сами
Для этого не нужен поставщик систем обнаружения. Откройте консоль браузера на любой странице и выполните:
function h() {
const c = document.createElement('canvas');
const x = c.getContext('2d');
x.textBaseline = 'top';
x.font = '14px Arial';
x.fillText('P8 canvas probe \u{1F511}', 2, 2);
return c.toDataURL().slice(-32);
}
console.log(h(), h(), h());
Три одинаковые строки — это то, что даст настоящий браузер. Три разные означают шум на каждый вызов, а значит, любой сайт, выполнивший те же три строки, об этом знает.
Затем выполните это снова после перезапуска профиля. Значение должно остаться прежним, если вы хотите, чтобы профиль выглядел как устройство, а не как подвижная мишень, — и отличаться от того, что дают все ваши другие профили. Сверьте результат с остальной частью отпечатка: стабильный canvas, который подразумевает видеокарту, не заявленную вашими строками WebGL, просто перенёс противоречие в другое место.