← 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;
}