init commit
This commit is contained in:
249
src/app/services/device-fingerprint.service.ts
Normal file
249
src/app/services/device-fingerprint.service.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Device Fingerprinting Service
|
||||
* Professional Angular Resume Builder - Browser Fingerprinting System
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @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<TwoFactorInitiationResponse | null>(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<DeviceVerificationResponse> {
|
||||
const fingerprint = this.generateFingerprint();
|
||||
const request: DeviceVerificationRequest = {
|
||||
userId,
|
||||
fingerprint
|
||||
};
|
||||
|
||||
return this.http.post<DeviceVerificationResponse>(`${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<TwoFactorInitiationResponse> {
|
||||
const request: TwoFactorInitiationRequest = {
|
||||
userId,
|
||||
method: 'email' // Default method for demo mode
|
||||
};
|
||||
|
||||
return this.http.post<TwoFactorInitiationResponse>(`${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<any> {
|
||||
const request: TwoFactorVerificationRequest = {
|
||||
code,
|
||||
verificationId,
|
||||
userId,
|
||||
tempToken // Include temp token if provided
|
||||
};
|
||||
|
||||
return this.http.post<any>(`${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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user