От давления к дистракции к ускорению

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

Если ЕС движется быстрее, чем техкомпании, это может означать только одно: техкомпании просто ждут. А одна из них контролирует две трети рынка браузеров.

Какой был план?

Поаплодируем Google — крупнейшей рекламной компании на земле!

АКТ I. ДАВЛЕНИЕ — "ПРИВАТНОСТЬ ДЛЯ НАС ИМЕЕТ ПЕРВОСТЕПЕННОЕ ЗНАЧЕНИЕ ВО ВСЕМ, ЧТО МЫ ДЕЛАЕМ"

2019 год, куки всё ещё в тренде. Репутация испортилась, Safari и Firefox уже их блокируют.

Через год Google дал обещание: мы уничтожим куки! И установим новый, privacy-first порядок.

АКТ II. ОТВЛЕЧЕНИЕ — "ВМЕСТО УДАЛЕНИЯ THIRD-PARTY COOKIES МЫ ПРЕДСТАВИМ НОВЫЙ ОПЫТ"

Пять лет спустя: людей теперь занимают куда более серьёзные проблемы, чем куки, а решения, которые разработал Google (Privacy Sandbox), просто неудачны и вызвали антимонопольные беспокойства — никто их не использует.

"Знаешь что, давай оставим куки. Зато мы замаскируем твой IP-адрес!" (* но только в режиме инкогнито при условии аутентификации) (** в итоге они даже это не доставили)

АКТ III. УСКОРЕНИЕ — ТЕПЕРЬ ВЫ МОЖЕТЕ ИСПОЛЬЗОВАТЬ DEVICE FINGERPRINTING В НАШИХ РЕКЛАМНЫХ ПРОДУКТАХ

Появился способ получше узнать, кто есть кто!

Как работает fingerprinting

Ладно, давайте разберёмся по-технически.

Пользователь заходит на сайт. Браузер отправляет запрос серверу, сервер возвращает код для выполнения браузером.

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

Затем код выполняется.

Мы добились большого прогресса в безопасности браузинга. Давно прошли времена, когда вирус можно было подхватить просто посетив сайт. Вопрос в том, как определяется безопасность. Общее согласие кончается на изоляции: код одного сайта не видит данные других сайтов и не может получить доступ к файлам или приложениям без разрешения. Chrome и Edge здесь и останавливаются. Safari (особенно на iOS/macOS 26+) и Firefox расширяют определение на приватность, но от fingerprinting они вас всё ещё не защищают полностью. Brave и Tor — лидеры по privacy-first подходу, с перспективным новичком Mullvad Browser (Mullvad, по мнению автора текста, один из лучших VPN-провайдеров).

Код выполняется на железе, которое можно измерить так, чтобы получить стабильные и уникальные показания. С этим мало что можно поделать, так что есть только два проверенных решения:

  1. добавлять немного шума. Это называется farbling, и именно это делает Brave, чтобы отпечаток был непоследовательным.
  2. сделать всё одинаковым всегда, чтобы нельзя было отличить одного юзера от другого. Этот подход используют Tor и Mullvad.

Создаём отпечаток

Есть два источника данных для отпечатка: сетевой запрос и окружение кода.

На уровне сети данные либо хрупкие, либо универсальные:

  • IP-адрес. В идеальном сценарии, когда человек всегда использует один IP, этого достаточно. Но обычно люди много чего браузят на мобильных сетях, делят WiFi, используют VPN или их домашний IP меняется провайдером. Хрупко.
  • заголовки типа User-Agent или Accept-Language раскрывают браузер, ОС, версии и язык.

Когда код выполняется на девайсе, можно измерить характеристики его железа (без запроса разрешений!) — и они удивительно стабильны:

  • GPU: поведение, вендор, драйвер
  • CPU: поведение, вендор, число потоков
  • размер памяти
  • данные дисплея: разрешение, пиксельное соотношение, частота обновления
  • обработка звука
  • модель девайса

На уровне ПО: установленные шрифты, расширения, информация о системе…

Миллионы девайсов используют свежий Chrome, миллионы Macов имеют чип M4, миллионы браузеров говорят по-французски.

Но когда смотреть на девайсы, где свежий Chrome на французском на M4, число резко падает. Добавьте измерения железа — и оно становится практически уникальным.

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

Почтовый индекс (выведен из IP), дата рождения (раскрыта в какой-то соцсети) и пол (заполняется везде) вместе достаточны, чтобы идентифицировать более 60% американцев.

Всегда можно добавить ещё одну переменную, чтобы сузить пул кандидатов.

Информационная энтропия

Играли когда-то в Акинатора? Вы думаете о персонаже, джин задаёт примерно двадцать вопросов — и как-то угадывает. Вот что такое fingerprinting в двух словах, только браузер отвечает на вопросы за вас. Когда разберётесь с информационной энтропией, магия Акинатора превратится в простой трюк.

Упростим с игрой, где нужно угадать персонажа на доске через вопросы типа "Мой персонаж — мужчина?". Если ответ "да", и половина персонажей на доске — мужчины, теперь рассматриваются только половина всех кандидатов. Можно сказать, что ответ дал вам log₂(100 / 50) = log₂(2) = 1 бит информации, потому что пул сократился в два раза.

Биты, полученные из одного ответа, называют его surprisal: чем реже ответ, тем больше информации.

  • черта, общая для 1 из 2 человек — 1 бит (мужчина),
  • 1 из 8 — 3 бита (усы),
  • 1 из 128 — 7 бит (один глаз).

Surprisal количественно оценивает ценность ответа, а энтропия — ценность вопроса. Если 1 человек из 128 одноглазый, вопрос про это даст "нет" 127 раз из 128.

Ответ Вероятность Surprisal Вклад в энтропию
Да 1/128 log₂(128 / 1) = 7 бит 1/128 x 7 = ~0.055
Нет 127/128 log₂(128 / 127) = ~0.0113 бит 127/128 x 0.0113 = ~0.011

Суммируя вклад каждого возможного ответа, сам вопрос стоит примерно 0.066 бит энтропии — это среднее количество бит, которые вы получите от ответа.

Когда объединяют независимые черты, просто складывают биты или перемножают соотношения — мужчина с усами и одним глазом означает 11 бит, или 1 из 2048. Увы, как знал бы вам Томас Байес, в реальности ничего не так просто. Усы — уже сильный индикатор мужчины, и мужчина вероятнее потеряет глаз. Черты не независимы, поэтому биты не складываются прямо.

Несколько интересных применений этого типа рассуждений:

  • В медицинской диагностике: позитивный результат от 99%-точного теста на болезнь, поражающую 0.01% населения, примерно в 100 раз более вероятно будет ошибкой, чем вы действительно болеете. Нужны дополнительные доказательства!
  • В криптографии: пароль из четырёх случайных слов так же стойкий, как восемь случайных символов клавиатуры (но гораздо легче запомнить!)
    xkcd 936
  • В криминалистике: выделить одного человека в городе миллион населением требует примерно 20 бит. Описание свидетеля, совпадающее с 1 человеком из 1000, даёт только 10 бит — остаётся тысяча подозреваемых.

Что я могу с этим сделать?

Протестировать браузер:

  • coveryourtracks.eff.org показывает, насколько уникален отпечаток, с энтропией каждой измеренной точки данных.

Если используется Chrome — это как ходить с ID на лбу.

Safari и Firefox предлагают некоторую защиту, но продвинутые трекеры вас всё ещё идентифицируют.

Brave, Tor и Mullvad Browser — пока единственные крупные браузеры, которые реализовали продвинутую защиту от fingerprinting.

Если не хочется менять браузер — понятно. Автор текста большой фанат Arc, а его снимают с поддержки, и privacy-функции там так и не появятся. Есть расширения типа JShelter — они нарушают скрипты fingerprinting, перехватывая вызовы браузерного API. Но они и ломают некоторые сайты. Та же проблема с Resist Fingerprinting в Firefox.

Парадокс в том, что privacy-расширения делают вас более уникальным, потому что их используют немногие. Умный трекер может это обнаружить, игнорировать испорченные сигналы и сосредоточиться на неповреждённых.

Никакое расширение не сравнится с защитой на уровне движка, встроенной прямо в браузер. Если не хотите быть отслеживаемым — нужно взять на себя обязательство.

Методы на этой странице

Просто создаём хеш из нескольких измерений девайса. Если что-то изменится — хеш теперь другой. Профессиональные приложения куда сложнее и способны поддерживать идентификацию, даже если отдельные переменные меняются.

  1. OfflineAudioContext

    Рендерит короткий тон через компрессор без воспроизведения; крошечные различия в результате зависят от звукового движка браузера и CPU.

    const off = new OfflineAudioContext(1, 5000, 44100);
    // … a 1 kHz triangle wave into a dynamics compressor …
    const data = (await Promise.race([off.startRendering(), timeout])).getChannelData(0);
    let sum = 0;
    for (let i = 4500; i < 5000; i++) sum += Math.abs(data[i]);
    return sum.toFixed(3);
    
  2. WebGL

    Читает вендор и модель GPU (без версий), что поддерживают GPU и её драйвер, пиксели маленького отрисованного треугольника.

    const gl = document.createElement('canvas').getContext('webgl');
    const dbg = gl.getExtension('WEBGL_debug_renderer_info');
    return gpuName(gl.getParameter(dbg ? dbg.UNMASKED_RENDERER_WEBGL : gl.RENDERER) || '');
    // "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)" -> "apple m4"
    // …
    return fnv1a(new TextEncoder().encode([...(gl.getSupportedExtensions() || []).sort(), ...limits, ...precision].join('|')));
    // …
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, px);
    
  3. canvas

    Рисует текст в нескольких шрифтах, эмодзи и градиент, затем читает пиксели; антиалиасинг и рендеринг шрифта зависят от ОС, GPU и шрифтов.

    ctx.font = '15px "Times New Roman", serif';
    ctx.fillText('Sphinx of black quartz 🦊', 4, 22);
    // …
    const data = ctx.getImageData(0, 0, 240, 60).data;
    
  4. Intl

    Часовой пояс и региональные настройки даты/чисел; VPN меняет IP-геолокацию, но не часовой пояс.

    const date = Intl.DateTimeFormat().resolvedOptions();
    // …
    return [date.timeZone, date.locale, date.calendar, date.numberingSystem, hourCycle, (1234567.891).toLocaleString()].join('/');
    
  5. navigator

    Количество ядер CPU, размер памяти (только Chrome) и язык браузера.

    return [navigator.hardwareConcurrency, navigator.deviceMemory, navigator.language].join('/');
    
  6. crypto.subtle

    Хеширует все значения SHA-256, и хеш выбирает черту и животное для ника.

    const digest = await crypto.subtle.digest('SHA-256', bytes);
    // …
    nickname: stranger || `${pick(TRAITS, hex, 0)} ${pick(ANIMALS, hex, 8)}`,