dprintjs
Version:
Generate unique device fingerprints using browser characteristics
195 lines (192 loc) • 7.17 kB
JavaScript
;
class dPrintId {
constructor() { }
static getInstance() {
if (!dPrintId._instance) {
dPrintId._instance = new dPrintId();
}
return dPrintId._instance;
}
async generateFingerprint(options = {}) {
const components = await this.collectComponents(options);
if (options.format === 'long') {
return this.generateLongFingerprint(components, options.salt);
}
return this.generateShortFingerprint(components, options.salt);
}
async collectComponents(options) {
const components = [];
try {
// Basic components (always included)
components.push(navigator.userAgent);
components.push(`${window.screen.height}x${window.screen.width}x${window.screen.colorDepth}`);
components.push(Intl.DateTimeFormat().resolvedOptions().timeZone);
components.push(navigator.language);
components.push(navigator.hardwareConcurrency?.toString() || '');
components.push(navigator.deviceMemory?.toString() || '');
components.push(navigator.platform);
// Optional components
if (options.includeCanvas !== false) {
const canvasData = await this.getCanvasFingerprint();
components.push(canvasData);
}
if (options.includeAudio !== false) {
const audioData = await this.getAudioFingerprint();
components.push(audioData);
}
if (options.includeWebGL !== false) {
const webglData = this.getWebGLFingerprint();
components.push(webglData);
}
// Add some additional entropy
components.push(new Date().getTimezoneOffset().toString());
components.push(this.getPlugins());
components.push(this.getTouchSupport());
}
catch (error) {
console.warn('Error collecting fingerprint components:', error);
}
return components;
}
async getCanvasFingerprint() {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx)
return '';
canvas.width = 200;
canvas.height = 50;
// Add a background
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#069';
ctx.fillRect(2, 2, 50, 50);
// Add text
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#069';
ctx.fillText('👋 Hello, world!', 4, 15);
// Add some shapes
ctx.strokeStyle = '#069';
ctx.beginPath();
ctx.moveTo(100, 10);
ctx.bezierCurveTo(120, 20, 140, 0, 160, 30);
ctx.stroke();
return canvas.toDataURL();
}
catch {
return '';
}
}
async getAudioFingerprint() {
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const analyser = audioContext.createAnalyser();
const gainNode = audioContext.createGain();
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
gainNode.gain.value = 0;
oscillator.type = 'triangle';
oscillator.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start(0);
const audioTimeData = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(audioTimeData);
oscillator.stop();
audioContext.close();
return Array.from(audioTimeData)
.slice(0, 5)
.map(x => x.toString())
.join(',');
}
catch {
return '';
}
}
getWebGLFingerprint() {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl)
return '';
const webgl = gl;
const data = [
webgl.getParameter(webgl.VENDOR),
webgl.getParameter(webgl.RENDERER),
webgl.getParameter(webgl.VERSION),
webgl.getParameter(webgl.SHADING_LANGUAGE_VERSION),
this.getWebGLExtensions(webgl),
].join('~');
return data;
}
catch {
return '';
}
}
getWebGLExtensions(gl) {
try {
const extensions = gl.getSupportedExtensions();
return extensions ? extensions.join(',') : '';
}
catch {
return '';
}
}
getPlugins() {
try {
const plugins = Array.from(navigator.plugins || [])
.map(p => [p.name, p.description, Array.from(p)])
.flat()
.join(',');
return plugins;
}
catch {
return '';
}
}
getTouchSupport() {
try {
const touchPoints = navigator.maxTouchPoints;
const touchEvent = 'ontouchstart' in window;
const touchList = 'TouchEvent' in window;
return `${touchPoints},${touchEvent},${touchList}`;
}
catch {
return '';
}
}
async generateLongFingerprint(components, salt) {
try {
const data = components.join('|||') + (salt || '');
const msgBuffer = new TextEncoder().encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
catch {
// Fallback to short fingerprint if crypto API is not available
return this.generateShortFingerprint(components, salt);
}
}
generateShortFingerprint(components, salt) {
const data = components.join('|||') + (salt || '');
let h1 = 0xdeadbeef;
let h2 = 0x41c6ce57;
for (let i = 0; i < data.length; i++) {
const ch = data.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
// Generate 8-character string
return (h2 >>> 0).toString(36).padStart(6, '0') +
(h1 >>> 0).toString(36).padStart(6, '0').slice(0, 2);
}
}
exports.dPrintId = dPrintId;
//# sourceMappingURL=index.cjs.map