Banx. Реализация отпечатков пальцев браузера в Angular.

Aleksandr Serenko
F.A.F.N.U.R
Published in
6 min readOct 18, 2021
Fingerprints on Angular

В данной статье поговорим о устройстве отпечатков пальцев браузера (browser fingerprint’s, сигнатуры). Рассмотрим некоторые техники определения пользователя, а также реализуем некоторые из сигнатур.

Главное назначение отпечатков пальцев браузера — однозначно идентифицировать пользователя приложения.

Современные браузеры предоставляют пользователям анонимность, что в свою очередь предоставляет мошенникам доступ к приложению. Фингерпринты служат одним из инструментов борьбы с мошенничеством (фродом).

Рассмотрим виды и устройство отпечатков пальцев в браузере.

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

Виды отпечатков пальцев браузера

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

На данный момент существует множество способов реализации снимков в браузере. Большинство из техник описаны на сайте — browserleaks.com, на котором и приведены сами фингерпринты.

Если немного покопаться во внутренностях, можно вытащить и их реализацию.

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

  • IP Address — определение IP адреса пользователя. В данном случае смотрится IP адрес клиента, а также все его IP адреса в локальной сети, которые можно получить с помощью webrtc (пример использования — webrtc fingerprint). Отмечу, что мобильный интернет часто построен на динамических IP, а значит, когда пользователь использует мобильную сеть, то скорее всего у него будет часто меняться IP.
  • Canvas — разные устройства по разному отображают данные в canvas. Поэтому это позволяет объединить одни устройства в четко идентифицируемые группы. Это хорошо работает с Android устройствами, однако iphone’ы будут вести себя почти идентично, что сводит полезность к нулю.
  • JavaScript — браузеры поддерживают разные возможности JS, поэтому можно использовать эту информацию для определения конкретных браузеров.
  • WebGL — каждый современный браузер умеет работать с 3D. Благодаря этому можно генерировать уникальное изображение и делать на его основании хеш. Как и в случае с Canvas, хорошо для разнотипных устройств.
  • Font —определение шрифтов клиента являлось одним из спасательных кругов, по определению клиентов в интернете. Так как некоторые приложения предоставляют свой набор шрифтов, то определение списка известных шрифтов, делает клиента очень уникальным. Однако, в последнее время количество сторонних приложений сокращается, то и уникальность данной техники снижается.
  • Geolocation API — определение геопозиции пользователя, позволяет определить географическое расположение пользователя (с учетом его VPN). Стоит отметить, что пользователь может выбирать — разрешать ли при приложению определять и отслеживать свое местоположение.
  • Features Detection — использование особенностей браузера для его идентификации. Реализация построена на анализе доступных/недоступных свойств браузера.
  • Content Filters — определение фильтров контента. В основном это использование тора и плагинов (adblock, ghostery, …). По включенным плагинам, можно отследить трафик в TOR сети, но это уже немного другое.

Также стоит упомянуть evercookie (supercookie) — вечные куки. Это набор подходов, смысл которых сделать все возможное, чтобы клиент не смог удалить сохраненный id из своего браузера. Реализация суперкук посторена на том, чтобы записать во все хранилища, базы данных и другие технологии, уникальный id. А после перезагрузки страницы, проверить его существование и если его где-то нет, записать снова. Именно механизм восстановления делает очистку очень проблематичным.

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

Одним из последних интересных проектов по отпечаткам пальцев — это подход основанных на кеше favicon’ов (jonasstrehle/supercookie):

Прежде чем перейти к реализации, рекомендую ознакомиться с проектом fingerprints (fingerprintjs/fingerprintjs).

Большая часть того, что доступно в настоящий момент, есть в проекте fingerprintsjs.

Определение шрифтов

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

Реализация может быть следующей:

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

Также если все шрифты были сохранены, установим соответствующий флаг — FONTS_DETECTED:

detect(): Observable<FontsFingerprint> {
return this.sessionAsyncStorage.getItem<boolean | null>(FONTS_DETECTED).pipe(
take(1),
switchMap((fontsDetected) => (fontsDetected ? this.getDataFromStorage() : this.getData()))
);
}

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

Метод получения данных из хранилища:

private getDataFromStorage(): Observable<FontsFingerprint> {
const data: Record<string, boolean> = {};
const state = this.sessionAsyncStorage.state;

FONTS_NAMES.forEach((fontName, index) => {
if (typeof state[index] === 'boolean') {
data[fontName] = !!state[index];
}
});

return of(data);
}

Для определения шрифтом будем смотреть следующее:

private getData(): Observable<FontsFingerprint> {
const data: Record<string, boolean> = {};

// Create node
const testNode = this.getTestNode();
const isUnknownSupport = this.isSupport(testNode, 'sdfnerlkfgnerherlopigreiuhgerg');

let checked = 1;
let sum = 0;
const average = (): number => {
return Math.round((sum * 10) / checked);
};
const state = this.sessionAsyncStorage.state;

return interval(10 + average()).pipe(
take(FONTS_NAMES.length),
tap((index) => {
const font = FONTS_NAMES[index];

if (state[font] != null) {
data[font] = state[font];
} else {
const startTime = new Date().getTime();
const supported = this.isSupport(testNode, font, isUnknownSupport);
data[font] = supported;

checked++;
sum += new Date().getTime() - startTime;
this.sessionAsyncStorage.setItem(font, supported);
}
}),
last(),
map(() => {
this.clear(testNode);
this.sessionAsyncStorage.setItem(FONTS_DETECTED, true);

return data;
})
);
}

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

let sum = 0;
const average = (): number => {
return Math.round((sum * 10) / checked);
};

И тогда новое определения шрифта будет только через 10 мс + среднее время определения:

interval(10 + average())

Метод getTestNode создает новый блок на странице, в котором будет рассчитываться высота блоков:

private getTestNode(): HTMLDivElement {
const testNode = this.document.createElement('div');
testNode.setAttribute('id', 'text-node');
testNode.style.cssText = 'overflow: hidden; height: 1px; width: 1px; position: absolute;';
this.document.body.appendChild(testNode);

return testNode;
}

Метод getFontSize добавляет в ноду текст и замеряет высоту:

private getFontSize(testNode: HTMLElement, font: string, family: string): number | null {
const fontFamilyId = !family ? font : `${font}, ${family}`;
// eslint-disable-next-line max-len
testNode.innerHTML = `<div style="position: absolute; width: 30000px; visibility: hidden;" id="parenttest"><div id="test" style="float: left; white-space: nowrap; font-family: ${fontFamilyId}">${TEST_STRING}</div></div>`;

const element = this.document.getElementById('test');
const style = element && this.document.defaultView ? this.document.defaultView.getComputedStyle(element, '') : null;

return style ? parseInt(style.width, 10) : null;
}

Метод isSupport соответственно добавляет в два блока шрифт и смотрит высоту:

private isSupport(testNode: HTMLElement, font: string, isUnknownSupport: boolean = false): boolean {
let support = true;
const serif = { font, family: 'serif' };
const sansSerif = { font, family: 'sans-serif' };

if (isUnknownSupport) {
serif.family = '';
sansSerif.font = '';
sansSerif.family = '';
}

try {
const currentSerif = this.getFontSize(testNode, serif.font, serif.family);
const currentSansSerif = this.getFontSize(testNode, sansSerif.font, sansSerif.family);

support = currentSerif === currentSansSerif;
if (isUnknownSupport) {
support = !support;
}
} catch (e) {
// IE8 fix
support = false;
}

return support;
}

В результате, это позволяет получить список доступных шрифтов.

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

export function md5(contents: string): string {
return createHash('md5').update(contents).digest('hex');
}

То есть, оставим имена шрифтов , которые присутствуют в системе и создадим на их основании сигнатуру:

const fingerprint = md5(
Object.keys(data)
.filter((key) => data[key])
.join(',')
);

где data — это объект вида: { Arial: true, Roboto: false, … }.

Так как статья является продолжением цикла статей, добавим сохранение отпечатков пальцев браузера на сервере:

Как видно, в API 3 endpoint’а:

  • сохранение шрифтов
  • сохранение canvas
  • сохранение геопозиции

Создание слепка canvas украдем из библиотеки fingerprints:

В данном случае, мы всего лишь обернули функции в сервис Angular.

Аналогично создадим сервис по определению геопозиции GeolocationDetectorService:

Сервис запрашивает доступ для определения геопозиции, и в случае успеха, отдает координаты, иначе возвращает NULL.

Для запуска fingerprint’ов будем использовать redux и создадим state:

Теперь для запуска определения всех отпечатков, достаточно вызвать метод run в фасаде FingerprintFacade.

Резюме

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

Ссылки

Вернуться к оглавлению — Введение.

Предыдущая статья —Создание трекера событий пользователя в Angular.

Следующая статья — Проектирование регистрации на Angular.

Все исходники находятся на github, в репозитории:

Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий тег — fingerprints.

Подписывайтесь на блог, чтобы не пропустить новые статьи про Angular, и веб-разработку. Medium | Telegram| VK | FB | Tw | Inst | Ln

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock