looood / src /lib /fingerprint-service.ts
looda3131's picture
Clean push without any binary history
cc276cc
import { storageService } from './storage-service';
const FINGERPRINT_UUID_KEY = 'device_fingerprint_uuid';
/**
* A service to generate a unique and reasonably stable fingerprint for the user's device/browser.
*/
class FingerprintService {
private isBrowser: boolean;
constructor() {
this.isBrowser = typeof window !== 'undefined';
}
/**
* Hashes a string using the SHA-256 algorithm.
* @param text The string to hash.
* @returns A promise that resolves to the hex-encoded hash string.
*/
private async digest(text: string): Promise<string> {
if (!this.isBrowser || !window.crypto?.subtle) {
// Fallback for non-browser or insecure contexts
let hash = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0; // Convert to 32bit integer
}
return Promise.resolve(hash.toString(16));
}
const data = new TextEncoder().encode(text);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
/**
* Gets a persistent unique identifier for the application instance from IndexedDB.
* Generates and stores one if it doesn't exist.
* @returns A promise that resolves to the UUID string.
*/
private async getPersistentUUID(): Promise<string> {
let uuid = await storageService.getCachedData<string>(FINGERPRINT_UUID_KEY);
if (!uuid) {
// A simple UUID generator
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
await storageService.saveCachedData(FINGERPRINT_UUID_KEY, uuid);
}
return uuid;
}
/**
* Collects various properties from the browser and device.
* @returns An object containing the collected properties.
*/
private async getComponentData(): Promise<Record<string, any>> {
if (!this.isBrowser) {
return { environment: 'non-browser' };
}
const screen = window.screen;
const navigator = window.navigator;
return {
// Hardware/OS properties
platform: navigator.platform,
cpuCores: navigator.hardwareConcurrency,
deviceMemory: (navigator as any).deviceMemory || 'unknown',
// Browser properties
userAgent: navigator.userAgent,
vendor: navigator.vendor,
// Screen properties
screenResolution: `${screen.width}x${screen.height}`,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
// Locale and time
timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
languages: navigator.languages,
// Persistent random component from IndexedDB
appUuid: await this.getPersistentUUID(),
};
}
/**
* Generates a device fingerprint by collecting components and hashing them.
* @param nativeComponents An optional object containing properties from a native environment (e.g., Android).
* @returns A promise that resolves to the SHA-256 fingerprint string.
*/
public async generate(nativeComponents?: Record<string, any>): Promise<string> {
const webComponents = await this.getComponentData();
// Merge web components with any provided native components
const allComponents = { ...webComponents, ...nativeComponents };
// Sort keys to ensure consistent stringification
const sortedKeys = Object.keys(allComponents).sort();
const jsonString = JSON.stringify(sortedKeys.map(key => allComponents[key]));
return this.digest(jsonString);
}
}
// Export a singleton instance of the service
export const fingerprintService = new FingerprintService();