UNPKG

capacitor-biometric-authentication

Version:

Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!

318 lines (276 loc) 8.1 kB
import { Capacitor } from '@capacitor/core'; export interface SessionData { token: string; expiresAt: number; metadata?: Record<string, unknown>; } export class SessionManager { private static readonly SESSION_KEY = 'biometric_auth_session'; private static readonly ENCRYPTION_KEY = 'biometric_auth_encryption'; /** * Generate a cryptographically secure random token */ static generateSecureToken(): string { if (Capacitor.isNativePlatform()) { // On native platforms, this is handled by the native implementation return ''; } // For web, use crypto API const array = new Uint8Array(32); crypto.getRandomValues(array); return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join( '' ); } /** * Store session data securely */ static async storeSession(session: SessionData): Promise<void> { if (Capacitor.isNativePlatform()) { // Native platforms handle this internally return; } // For web, use sessionStorage with encryption const encryptedData = await this.encrypt(JSON.stringify(session)); sessionStorage.setItem(this.SESSION_KEY, encryptedData); } /** * Retrieve session data */ static async getSession(): Promise<SessionData | null> { if (Capacitor.isNativePlatform()) { // Native platforms handle this internally return null; } const encryptedData = sessionStorage.getItem(this.SESSION_KEY); if (!encryptedData) { return null; } try { const decryptedData = await this.decrypt(encryptedData); const session = JSON.parse(decryptedData) as SessionData; // Check if session is expired if (session.expiresAt < Date.now()) { await this.clearSession(); return null; } return session; } catch (error) { console.error('Failed to decrypt session:', error); await this.clearSession(); return null; } } /** * Clear session data */ static async clearSession(): Promise<void> { if (Capacitor.isNativePlatform()) { // Native platforms handle this internally return; } sessionStorage.removeItem(this.SESSION_KEY); } /** * Check if session is valid */ static async isSessionValid(): Promise<boolean> { const session = await this.getSession(); return session !== null && session.expiresAt > Date.now(); } /** * Extend session expiration */ static async extendSession(durationMs: number): Promise<boolean> { const session = await this.getSession(); if (!session) { return false; } session.expiresAt = Date.now() + durationMs; await this.storeSession(session); return true; } /** * Simple encryption using Web Crypto API (for demonstration) * In production, use a proper encryption library or service */ private static async encrypt(data: string): Promise<string> { if (!crypto.subtle) { // Fallback to base64 if crypto.subtle is not available return btoa(data); } try { const encoder = new TextEncoder(); const dataBuffer = encoder.encode(data); // Generate encryption key from string const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(this.ENCRYPTION_KEY), { name: 'PBKDF2' }, false, ['deriveBits', 'deriveKey'] ); const key = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: encoder.encode('biometric-auth-salt'), iterations: 100000, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); // Generate IV const iv = crypto.getRandomValues(new Uint8Array(12)); // Encrypt const encryptedData = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, dataBuffer ); // Combine IV and encrypted data const combined = new Uint8Array(iv.length + encryptedData.byteLength); combined.set(iv); combined.set(new Uint8Array(encryptedData), iv.length); // Convert to base64 return btoa(String.fromCharCode(...combined)); } catch (error) { console.error('Encryption failed:', error); // Fallback to base64 return btoa(data); } } /** * Simple decryption using Web Crypto API */ private static async decrypt(encryptedData: string): Promise<string> { if (!crypto.subtle) { // Fallback from base64 if crypto.subtle is not available return atob(encryptedData); } try { const encoder = new TextEncoder(); const decoder = new TextDecoder(); // Convert from base64 const combined = Uint8Array.from(atob(encryptedData), (c) => c.charCodeAt(0) ); // Extract IV and encrypted data const iv = combined.slice(0, 12); const encrypted = combined.slice(12); // Generate decryption key const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(this.ENCRYPTION_KEY), { name: 'PBKDF2' }, false, ['deriveBits', 'deriveKey'] ); const key = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: encoder.encode('biometric-auth-salt'), iterations: 100000, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); // Decrypt const decryptedData = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, encrypted ); return decoder.decode(decryptedData); } catch (error) { console.error('Decryption failed:', error); // Try fallback from base64 return atob(encryptedData); } } } /** * Credential storage utilities */ export class CredentialManager { private static readonly CREDENTIAL_PREFIX = 'biometric_credential_'; /** * Store credential securely */ static async storeCredential( credentialId: string, credentialData: unknown, encrypt: boolean = true ): Promise<void> { const key = `${this.CREDENTIAL_PREFIX}${credentialId}`; const dataStr = JSON.stringify(credentialData); if (encrypt && crypto.subtle) { const encryptedData = await SessionManager['encrypt'](dataStr); localStorage.setItem(key, encryptedData); } else { localStorage.setItem(key, dataStr); } } /** * Retrieve credential */ static async getCredential( credentialId: string, decrypt: boolean = true ): Promise<unknown | null> { const key = `${this.CREDENTIAL_PREFIX}${credentialId}`; const storedData = localStorage.getItem(key); if (!storedData) { return null; } try { if (decrypt && crypto.subtle) { const decryptedData = await SessionManager['decrypt'](storedData); return JSON.parse(decryptedData); } else { return JSON.parse(storedData); } } catch (error) { console.error('Failed to retrieve credential:', error); return null; } } /** * Delete credential */ static deleteCredential(credentialId: string): void { const key = `${this.CREDENTIAL_PREFIX}${credentialId}`; localStorage.removeItem(key); } /** * List all credential IDs */ static listCredentialIds(): string[] { const ids: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(this.CREDENTIAL_PREFIX)) { ids.push(key.substring(this.CREDENTIAL_PREFIX.length)); } } return ids; } /** * Clear all credentials */ static clearAllCredentials(): void { const keys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(this.CREDENTIAL_PREFIX)) { keys.push(key); } } keys.forEach((key) => localStorage.removeItem(key)); } }