← Все статьи

Аудиоотпечаток, разобранный по механике

4 мин чтения fingerprintingaudioguide

Скрипт собирает короткий граф обработки звука, осциллятор, подающий сигнал в компрессор, рендерит его офлайн настолько быстро, насколько позволяет процессор, и считывает получившийся буфер отсчётов. Ничего не воспроизводится через динамик, и запроса разрешения не появляется, потому что офлайн-рендеринг через Web Audio API - чистое вычисление, а не воспроизведение.

Как это устроено

OscillatorNode генерирует периодический сигнал, обычно треугольный, на фиксированной частоте и заданной длительности. Этот сигнал проходит через DynamicsCompressorNode с фиксированными параметрами (порог, колено, отношение, атака, восстановление) - тот самый узел, которым звукорежиссёр укрощает пики, здесь применённый ради его нелинейности: снижение усиления считается непрерывной математикой по каждому отсчёту, поэтому мелкие различия в реализации не усредняются по буферу, а накапливаются.

Весь граф работает внутри OfflineAudioContext, который рендерит как можно быстрее, а не в реальном времени, и разрешается объектом AudioBuffer. Страница читает getChannelData(0), то есть Float32Array, и сводит его к чему-то короткому: сумме модулей по срезу отсчётов, хешу сырых байтов или конкретным индексам, прочитанным напрямую. Конкретный способ свёртки у скриптов разный. Источник энтропии - нет.

Почему один и тот же сигнал даёт разные числа на разных машинах

Инструкция «прогони этот осциллятор через этот компрессор» не определяет результат на уровне битов. Кривая усиления компрессора построена на логарифмах и экспонентах, а то, как они вычисляются, зависит от аудиостека ОС, внутреннего кода DSP и передискретизации в браузере, использует ли путь на процессоре SIMD, и от поведения округления с плавающей точкой, которое различается между железом и версиями софта. Смените любой слой этой цепочки - и значения отсчётов сдвинутся, иногда лишь в последних битах, чего достаточно, чтобы хеш поменялся целиком.

Та же цепочка не меняется между двумя рендерами в одной сессии на одной машине. Тот же процессор, тот же аудиостек ОС, та же сборка браузера, каждый раз, - поэтому значение стабильно у вас и отличается у другого. Оно измеряет детерминированное вычисление, а не случайное.

Тише, чем canvas, и подделать труднее

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

Подделать его убедительно тоже труднее, и по конкретной причине. Правдоподобному значению canvas достаточно выглядеть похожим на пиксельный шум. Значение аудио должно быть тем, что настоящий компрессор на конкретном процессоре и аудиостеке ОС действительно выдал бы, вплоть до округления с плавающей точкой. Выдать просто правдоподобное число легко. Выдать число, остающееся внутренне согласованным со всем остальным, что профиль заявляет о своём железе и ОС, - другая, куда более трудная задача, потому что энтропия рождается из деталей реализации, которых большинство инструментов подмены вообще не касается.

Те же три вида отказа, что и у canvas

Отсутствие. Переопределение AudioContext или OfflineAudioContext так, чтобы метод бросал исключение или ничего не возвращал. Настоящие браузеры так почти не делают, поэтому отсутствующий аудиоотпечаток сам по себе редкий опознающий сигнал.

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

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

Это те же три вида отказа, которые подробнее разобраны в статье про отпечаток canvas, потому что подложечная логика - детерминированность на машине и различие между машинами - у обоих сигналов одинакова.

Что выдерживает проверку

Значение, полученное настоящим конвейером рендеринга для этого конкретного профиля, применённое в самом движке браузера, ниже слоя JavaScript, сгенерированное один раз на профиль и сохранённое, а не пересчитываемое и не переопределяемое скриптом, сидящим поверх настоящего API.

Когда правильный ответ - инструмент попроще

Если цель - личная приватность в одном браузере, а не ведение множества различных профилей, режим resist-fingerprinting в Firefox нормализует вывод AudioContext в рамках той же защиты, что применяет к canvas, бесплатно и без расширений. Tor Browser наследует ту же защиту. Ни то, ни другое не поможет, если вам нужно несколько профилей, каждый из которых обязан выглядеть отдельной настоящей машиной, ведь нормализация к общему значению - противоположность этому сценарию; но для одного человека под одной личностью это разумный и бесплатный ответ.

Прочитайте своё значение

Выполните это в консоли браузера:

async function audioHash() {
  const ctx = new OfflineAudioContext(1, 5000, 44100)
  const oscillator = ctx.createOscillator()
  oscillator.type = 'triangle'
  oscillator.frequency.value = 10000
  const compressor = ctx.createDynamicsCompressor()
  compressor.threshold.value = -50
  compressor.knee.value = 40
  compressor.ratio.value = 12
  compressor.attack.value = 0
  compressor.release.value = 0.25
  oscillator.connect(compressor)
  compressor.connect(ctx.destination)
  oscillator.start(0)
  const buffer = await ctx.startRendering()
  const data = buffer.getChannelData(0)
  let sum = 0
  for (let i = 4500; i < 5000; i++) sum += Math.abs(data[i])
  return sum
}
audioHash().then(console.log)
audioHash().then(console.log)

Два выведенных числа на настоящем немодифицированном браузере обязаны совпасть в точности. Если вы проверяете инструмент против детекта и они различаются, значит инструмент подменяет вывод в реальном времени, а это опознаёт сильнее любого значения, которое он пытался спрятать.

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

Попробуйте на бесплатном тарифе.

Неограниченное число локальных профилей, без карты. Проверьте результат детекторами сами.