← back to Watches

public/biometric-auth.js

416 lines

/**
 * Biometric Authentication Module
 * Supports fingerprint, Face ID, Touch ID, and platform authenticators
 * Uses Web Authentication API (WebAuthn)
 */

class BiometricAuth {
  constructor() {
    this.isAvailable = false;
    this.publicKey = null;
    this.currentUser = null;
    this.checkAvailability();
  }

  /**
   * Check if biometric authentication is available
   */
  async checkAvailability() {
    this.isAvailable = !!(
      window.PublicKeyCredential &&
      navigator.credentials &&
      navigator.credentials.create
    );

    if (this.isAvailable) {
      // Check for platform authenticator (Face ID, Touch ID, Windows Hello)
      this.hasPlatformAuthenticator = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
      console.log('[BiometricAuth] Available:', this.isAvailable);
      console.log('[BiometricAuth] Platform authenticator:', this.hasPlatformAuthenticator);
    } else {
      console.warn('[BiometricAuth] Not available on this device');
    }

    return this.isAvailable;
  }

  /**
   * Register biometric credentials
   */
  async register(username, displayName) {
    if (!this.isAvailable) {
      throw new Error('Biometric authentication not available');
    }

    try {
      // Generate challenge from server (in production)
      const challenge = this.generateChallenge();

      const publicKeyOptions = {
        challenge: challenge,
        rp: {
          name: 'Omega Watch Price History',
          id: window.location.hostname
        },
        user: {
          id: this.stringToArrayBuffer(username),
          name: username,
          displayName: displayName || username
        },
        pubKeyCredParams: [
          { type: 'public-key', alg: -7 },  // ES256
          { type: 'public-key', alg: -257 } // RS256
        ],
        authenticatorSelection: {
          authenticatorAttachment: 'platform',
          requireResidentKey: false,
          userVerification: 'required'
        },
        timeout: 60000,
        attestation: 'direct'
      };

      const credential = await navigator.credentials.create({
        publicKey: publicKeyOptions
      });

      // Save credential to IndexedDB
      await this.saveCredential(username, credential);

      console.log('[BiometricAuth] Registration successful');

      return {
        success: true,
        credentialId: this.arrayBufferToBase64(credential.rawId),
        message: 'Biometric authentication registered successfully'
      };

    } catch (error) {
      console.error('[BiometricAuth] Registration failed:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }

  /**
   * Authenticate using biometrics
   */
  async authenticate(username) {
    if (!this.isAvailable) {
      throw new Error('Biometric authentication not available');
    }

    try {
      // Get stored credentials
      const storedCredential = await this.getStoredCredential(username);

      if (!storedCredential) {
        throw new Error('No credentials found for this user');
      }

      const challenge = this.generateChallenge();

      const publicKeyOptions = {
        challenge: challenge,
        timeout: 60000,
        rpId: window.location.hostname,
        allowCredentials: [{
          type: 'public-key',
          id: this.base64ToArrayBuffer(storedCredential.credentialId),
          transports: ['internal']
        }],
        userVerification: 'required'
      };

      const assertion = await navigator.credentials.get({
        publicKey: publicKeyOptions
      });

      // Verify assertion (in production, verify on server)
      const verified = await this.verifyAssertion(assertion, storedCredential);

      if (verified) {
        this.currentUser = username;
        console.log('[BiometricAuth] Authentication successful');

        // Update last login time
        await this.updateLastLogin(username);

        return {
          success: true,
          username: username,
          message: 'Authentication successful'
        };
      } else {
        throw new Error('Verification failed');
      }

    } catch (error) {
      console.error('[BiometricAuth] Authentication failed:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }

  /**
   * Quick authenticate (use stored credentials)
   */
  async quickAuth() {
    if (!this.isAvailable) {
      return { success: false, error: 'Not available' };
    }

    try {
      const lastUser = await this.getLastUser();

      if (!lastUser) {
        return { success: false, error: 'No previous login found' };
      }

      return await this.authenticate(lastUser);
    } catch (error) {
      console.error('[BiometricAuth] Quick auth failed:', error);
      return { success: false, error: error.message };
    }
  }

  /**
   * Check if user has registered biometrics
   */
  async hasCredentials(username) {
    const credential = await this.getStoredCredential(username);
    return !!credential;
  }

  /**
   * Remove biometric credentials
   */
  async removeCredentials(username) {
    try {
      const db = await this.openDB();
      const tx = db.transaction('biometric', 'readwrite');
      const store = tx.objectStore('biometric');

      await new Promise((resolve, reject) => {
        const request = store.delete(username);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });

      console.log('[BiometricAuth] Credentials removed');
      return true;
    } catch (error) {
      console.error('[BiometricAuth] Failed to remove credentials:', error);
      return false;
    }
  }

  /**
   * Save credential to IndexedDB
   */
  async saveCredential(username, credential) {
    const db = await this.openDB();
    const tx = db.transaction('biometric', 'readwrite');
    const store = tx.objectStore('biometric');

    const credentialData = {
      userId: username,
      credentialId: this.arrayBufferToBase64(credential.rawId),
      publicKey: this.arrayBufferToBase64(credential.response.getPublicKey()),
      attestationObject: this.arrayBufferToBase64(credential.response.attestationObject),
      createdAt: Date.now(),
      lastLogin: null
    };

    return new Promise((resolve, reject) => {
      const request = store.put(credentialData);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Get stored credential
   */
  async getStoredCredential(username) {
    const db = await this.openDB();
    const tx = db.transaction('biometric', 'readonly');
    const store = tx.objectStore('biometric');

    return new Promise((resolve, reject) => {
      const request = store.get(username);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Update last login time
   */
  async updateLastLogin(username) {
    const db = await this.openDB();
    const tx = db.transaction('biometric', 'readwrite');
    const store = tx.objectStore('biometric');

    const credential = await this.getStoredCredential(username);

    if (credential) {
      credential.lastLogin = Date.now();

      return new Promise((resolve, reject) => {
        const request = store.put(credential);
        request.onsuccess = () => resolve();
        request.onerror = () => reject(request.error);
      });
    }
  }

  /**
   * Get last logged in user
   */
  async getLastUser() {
    const db = await this.openDB();
    const tx = db.transaction('biometric', 'readonly');
    const store = tx.objectStore('biometric');

    return new Promise((resolve, reject) => {
      const request = store.getAll();
      request.onsuccess = () => {
        const credentials = request.result;

        if (credentials.length === 0) {
          resolve(null);
          return;
        }

        // Find most recently logged in user
        const lastUser = credentials.reduce((prev, current) => {
          return (prev.lastLogin > current.lastLogin) ? prev : current;
        });

        resolve(lastUser.userId);
      };
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Verify assertion (simplified - in production, verify on server)
   */
  async verifyAssertion(assertion, storedCredential) {
    // In production, send to server for verification
    // For now, just check that we got a valid response
    return !!(
      assertion &&
      assertion.response &&
      assertion.response.authenticatorData &&
      assertion.response.signature
    );
  }

  /**
   * Generate challenge
   */
  generateChallenge() {
    const challenge = new Uint8Array(32);
    window.crypto.getRandomValues(challenge);
    return challenge;
  }

  /**
   * Helper: String to ArrayBuffer
   */
  stringToArrayBuffer(str) {
    const encoder = new TextEncoder();
    return encoder.encode(str);
  }

  /**
   * Helper: ArrayBuffer to Base64
   */
  arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    bytes.forEach(byte => binary += String.fromCharCode(byte));
    return window.btoa(binary);
  }

  /**
   * Helper: Base64 to ArrayBuffer
   */
  base64ToArrayBuffer(base64) {
    const binary = window.atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }

  /**
   * Open IndexedDB
   */
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('OmegaWatchesPWA', 2);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        if (!db.objectStoreNames.contains('biometric')) {
          db.createObjectStore('biometric', { keyPath: 'userId' });
        }
      };
    });
  }

  /**
   * Get authentication type available
   */
  getAuthType() {
    if (!this.isAvailable) return 'none';

    const platform = navigator.platform.toLowerCase();
    const userAgent = navigator.userAgent.toLowerCase();

    if (platform.includes('mac') || userAgent.includes('mac')) {
      return 'Touch ID';
    } else if (platform.includes('iphone') || platform.includes('ipad')) {
      return 'Face ID / Touch ID';
    } else if (platform.includes('win')) {
      return 'Windows Hello';
    } else if (userAgent.includes('android')) {
      return 'Fingerprint';
    } else {
      return 'Biometric';
    }
  }

  /**
   * Logout current user
   */
  logout() {
    this.currentUser = null;
    console.log('[BiometricAuth] User logged out');
  }

  /**
   * Get current user
   */
  getCurrentUser() {
    return this.currentUser;
  }
}

// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
  module.exports = BiometricAuth;
}