Отпечаток по шрифтам, разобранный по механике
Большинство браузеров не отдают странице список установленных шрифтов по запросу. Поэтому страница задаёт вопрос поуже, снова и снова: установлен ли конкретно этот шрифт, да или нет, - и отвечает на него измерением текста, а не чтением списка.
Техника измерения
Отрисуйте фиксированную тестовую строку шрифтом-кандидатом с общим запасным позади, "Candidate Font", monospace, и отдельно только запасным, monospace. Если шрифт-кандидат установлен, браузер подставит его, и отрисованный текст измерится иначе - по ширине, а часто и по высоте, - чем базовая линия только с запасным. Если не установлен, браузер оба раза провалится в запасной, и два измерения совпадут.
function isFontAvailable(font) {
const testString = 'mmmmmmmmmmlli'
const testSize = '72px'
const fallbacks = ['monospace', 'sans-serif', 'serif']
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const baselineWidths = fallbacks.map(fallback => {
ctx.font = `${testSize} ${fallback}`
return ctx.measureText(testString).width
})
return fallbacks.some((fallback, i) => {
ctx.font = `${testSize} "${font}", ${fallback}`
return ctx.measureText(testString).width !== baselineWidths[i]
})
}
console.log('Arial', isFontAvailable('Arial'))
console.log('Comic Sans MS', isFontAvailable('Comic Sans MS'))
console.log('SF Pro Display', isFontAvailable('SF Pro Display'))
Прогоните это по каждому кандидату из короткого списка в несколько сотен распространённых имён - и ответы «да» и «нет» вместе составят набор: шрифты, доступные этому конкретному браузеру. Более старые реализации той же идеи использовали скрытый span за пределами экрана и сравнивали offsetWidth/offsetHeight с теми же тремя базовыми линиями. Версия на canvas выше делает то же сравнение, не трогая DOM.
document.fonts.check()
CSS Font Loading API даёт более прямую версию того же вопроса: document.fonts.check('12px "Font Name"') возвращает булево значение о доступности шрифта для отрисовки, и странице не нужно строить собственное сравнение с запасным. Это дешевле техники с измерением и даёт то же «да» или «нет» по одному кандидату за раз, а не список целиком.
console.log(document.fonts.check('12px "Arial"'))
console.log(document.fonts.check('12px "SF Pro Display"'))
Local Font Access API
Более новый API за разрешением позволяет странице не щупать, а спросить напрямую: window.queryLocalFonts() возвращает настоящие метаданные шрифтов - имя семейства, полное имя, postscript-имя - прямо из системного списка. В отличие от техник выше, он требует явно выданного разрешения, прежде чем что-то вернёт, так что скрипт не может получить этот список молча при загрузке страницы; поэтому сбор отпечатков по-прежнему опирается на щупание, а не на этот API.
Почему набор установленных шрифтов так опознаёт
Доступные браузеру шрифты - не свойство браузера, а запись о машине под ним. Системы Windows поставляются со шрифтами вроде Calibri и Segoe UI. macOS несёт San Francisco и Helvetica Neue. Дистрибутивы Linux обычно ставят по умолчанию семейства DejaVu и Noto, и точный набор зависит от дистрибутива. Шрифты локалей и языковых пакетов появляются, только если эта языковая поддержка была установлена, и яснее всего это видно на письменностях CJK и индийских. Установленный софт добавляет ещё: дизайнерские и издательские инструменты несут свои библиотеки шрифтов, офисные пакеты добавляют семейства сверх системных. Ничто из этого не имеет отношения к кукам или IP-адресам, и всё это читается несколькими сотнями сравнений ширины.
Межплатформенные несовпадения - самый громкий признак
Строка user agent, заявляющая Windows, в паре со шрифтами, которые бывают только на macOS, - не маловероятное сочетание. Это невозможное сочетание. Эти файлы шрифтов живут в собственных каталогах шрифтов операционной системы, а не там, куда их ставит страница или скрипт, поэтому у настоящей машины на Windows нет законного пути обзавестись ими. Детектору здесь не нужно рассуждать о вероятностях, как в случае с нечастым разрешением экрана или необычным часовым поясом. Либо шрифты соответствуют той ОС, которой браузер себя называет, либо нет, и промежуточного варианта, которым можно было бы прикрыться, не существует.
Подозрительно короткий список шрифтов - сигнал сам по себе
На настоящих десктопных машинах десятки шрифтов только из системных, ещё до всякого стороннего софта. Профиль, который отвечает «не установлен» почти на всё и совпадает только с тремя общими семействами, выглядит не приватным, а таким, где шрифты блокируют или где окружение - урезанный образ без ничего установленного. Блокировка перечисления шрифтов убирает конкретный сигнал ценой создания другого: пустого списка шрифтов почти ни у кого из настоящих пользователей нет, так что пустой список сам по себе редок и опознаёт, - тот же самый размен, что и у заблокированного чтения canvas.
Что выдерживает проверку
Набор шрифтов, согласованный с ОС, локалью и языковыми пакетами, которые заявляет остальной профиль, сгенерированный один раз на профиль и применённый в самом движке браузера, а не собранный скриптом, которому приходится угадывать, какие шрифты реально были бы у машины с таким описанием.
Когда правильный ответ - инструмент попроще
Режим resist-fingerprinting в Firefox ограничивает перечисление шрифтов фиксированным списком, зашитым в браузер, одинаковым независимо от того, что реально установлено внизу, и это бесплатно убирает данный источник энтропии. Компромисс в том, что страница, запросившая шрифт вне этого списка, его не получит, поэтому часть текста отрисуется запасным вместо задуманного, - нормальный размен для одного человека, который просто хочет приватности. Тому, кому нужно несколько профилей, каждый из которых обязан выглядеть своей отдельной машиной, это не поможет: общий список по построению делает все профили совпадающими по шрифтам.
Проверьте свой набор
Помимо фрагментов выше, CreepJS выполняет более широкую пробу шрифтов рядом с остальными проверками и отмечает отдельными находками и пустой список, и шрифты, не согласованные с ОС. Как согласованность шрифтов вписывается в более широкий набор свойств, обязанных сходиться друг с другом, - в чек-листе согласованности отпечатка, а та же логика «детерминировано на машине» на другом API - в статье про отпечаток canvas.
Попробуйте на бесплатном тарифе.
Неограниченное число локальных профилей, без карты. Проверьте результат детекторами сами.