/** * Device Fingerprinting Service * Professional Angular Resume Builder - Browser Fingerprinting System * * @author David Valera Melendez * @created 2025-08-08 * @location Made in Germany 🇩🇪 */ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, BehaviorSubject, throwError } from 'rxjs'; import { map, catchError, tap } from 'rxjs/operators'; import { environment } from '../../environments/environment'; import { DeviceFingerprint, DeviceVerificationRequest, DeviceVerificationResponse, TwoFactorInitiationRequest, TwoFactorInitiationResponse, TwoFactorVerificationRequest } from '../models/auth.model'; /** * Service for generating device fingerprints and managing device trust */ @Injectable({ providedIn: 'root' }) export class DeviceFingerprintService { private readonly apiUrl = `${environment.apiUrl}/device-fingerprint`; // State management for 2FA process private currentTwoFactorSession = new BehaviorSubject(null); public readonly currentTwoFactorSession$ = this.currentTwoFactorSession.asObservable(); constructor(private http: HttpClient) {} /** * Generate browser fingerprint from available browser APIs */ generateFingerprint(): DeviceFingerprint { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let canvasFingerprint = ''; if (ctx) { ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillText('Device fingerprint test 🔒', 2, 2); canvasFingerprint = canvas.toDataURL().slice(-50); } // Collect available fonts (simplified approach) const testFonts = ['Arial', 'Helvetica', 'Times', 'Georgia', 'Verdana', 'Courier']; const availableFonts = testFonts.filter(font => this.isFontAvailable(font)); const fontsHash = this.hashString(availableFonts.join(',')); const fingerprint: DeviceFingerprint = { userAgent: navigator.userAgent, acceptLanguage: navigator.language || (navigator as any).userLanguage || '', screenResolution: `${screen.width}x${screen.height}`, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, doNotTrack: navigator.doNotTrack === '1', fontsHash: fontsHash, canvasFingerprint: canvasFingerprint, webglFingerprint: this.getWebGLFingerprint() }; return fingerprint; } catch (error) { console.error('Error generating fingerprint:', error); // Return minimal fingerprint on error return { userAgent: navigator.userAgent, acceptLanguage: navigator.language || '', screenResolution: `${screen.width}x${screen.height}`, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, platform: navigator.platform, cookieEnabled: navigator.cookieEnabled, doNotTrack: false }; } } /** * Verify device trust status with backend */ verifyDevice(userId: number): Observable { const fingerprint = this.generateFingerprint(); const request: DeviceVerificationRequest = { userId, fingerprint }; return this.http.post(`${this.apiUrl}/verify`, request) .pipe( catchError(error => { console.error('Device verification failed:', error); return throwError(() => error); }) ); } /** * Initiate two-factor authentication for device registration */ initiateTwoFactor(userId: number, deviceName?: string): Observable { const request: TwoFactorInitiationRequest = { userId, method: 'email' // Default method for demo mode }; return this.http.post(`${this.apiUrl}/two-factor/initiate`, request) .pipe( tap(response => { // Store the 2FA session this.currentTwoFactorSession.next(response); }), catchError(error => { console.error('2FA initiation failed:', error); return throwError(() => error); }) ); } /** * Verify two-factor authentication code */ verifyTwoFactor(code: string, verificationId: string, userId: number, tempToken?: string): Observable { const request: TwoFactorVerificationRequest = { code, verificationId, userId, tempToken // Include temp token if provided }; return this.http.post(`${this.apiUrl}/two-factor/verify`, request) .pipe( tap(response => { // Clear the 2FA session on success if (response.success) { this.currentTwoFactorSession.next(null); } }), catchError(error => { console.error('2FA verification failed:', error); return throwError(() => error); }) ); } /** * Clear current 2FA session */ clearTwoFactorSession(): void { this.currentTwoFactorSession.next(null); } /** * Check if a font is available by measuring text width */ private isFontAvailable(fontName: string): boolean { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return false; const baselineText = 'abcdefghijklmnopqrstuvwxyz'; ctx.font = '32px monospace'; const baselineWidth = ctx.measureText(baselineText).width; ctx.font = `32px ${fontName}, monospace`; const testWidth = ctx.measureText(baselineText).width; return baselineWidth !== testWidth; } /** * Get WebGL fingerprint for additional uniqueness */ private getWebGLFingerprint(): string { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') as WebGLRenderingContext; if (!gl) return ''; const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); if (debugInfo) { const vendor = gl.getParameter((debugInfo as any).UNMASKED_VENDOR_WEBGL); const renderer = gl.getParameter((debugInfo as any).UNMASKED_RENDERER_WEBGL); return this.hashString(`${vendor}|${renderer}`).slice(0, 16); } return ''; } catch (error) { return ''; } } /** * Generate device name based on browser and platform info */ private getDeviceName(): string { const userAgent = navigator.userAgent; let deviceName = 'Unknown Device'; if (userAgent.includes('Windows')) { deviceName = 'Windows Device'; } else if (userAgent.includes('Mac')) { deviceName = 'Mac Device'; } else if (userAgent.includes('Linux')) { deviceName = 'Linux Device'; } else if (userAgent.includes('Android')) { deviceName = 'Android Device'; } else if (userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')) { deviceName = 'iOS Device'; } // Add browser info if (userAgent.includes('Chrome')) { deviceName += ' (Chrome)'; } else if (userAgent.includes('Firefox')) { deviceName += ' (Firefox)'; } else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) { deviceName += ' (Safari)'; } else if (userAgent.includes('Edge')) { deviceName += ' (Edge)'; } return deviceName; } /** * Simple string hashing function */ private hashString(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } }