mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-04 06:52:16 -04:00
217 lines
7.8 KiB
TypeScript
217 lines
7.8 KiB
TypeScript
import { Platform } from 'react-native';
|
|
import * as LocalAuthentication from 'expo-local-authentication';
|
|
import NativeVaultManager from '@/specs/NativeVaultManager';
|
|
import i18n from '@/i18n';
|
|
|
|
export type AuthMethod = 'faceid' | 'password';
|
|
|
|
/**
|
|
* Comprehensive utility for all app unlock-related functionality.
|
|
* Centralizes biometric availability checks, display name logic, auth method management,
|
|
* and unlock configuration.
|
|
*/
|
|
export class AppUnlockUtility {
|
|
/**
|
|
* Get enabled auth methods from the native module.
|
|
* Filters out Face ID if biometrics are not enrolled.
|
|
*/
|
|
static async getEnabledAuthMethods(): Promise<AuthMethod[]> {
|
|
try {
|
|
let methods = await NativeVaultManager.getAuthMethods() as AuthMethod[];
|
|
// Check if Face ID is actually available despite being enabled
|
|
if (methods.includes('faceid')) {
|
|
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
|
|
if (!isEnrolled) {
|
|
// Remove Face ID from the list of enabled auth methods
|
|
methods = methods.filter(method => method !== 'faceid');
|
|
}
|
|
}
|
|
return methods;
|
|
} catch (error) {
|
|
console.error('Failed to get enabled auth methods:', error);
|
|
return ['password'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the authentication methods and save them to native storage.
|
|
* Always ensures password is included as a fallback.
|
|
*/
|
|
static async setAuthMethods(methods: AuthMethod[]): Promise<void> {
|
|
// Ensure password is always included
|
|
const methodsToSave = methods.includes('password') ? methods : [...methods, 'password'];
|
|
|
|
// Update native credentials manager
|
|
try {
|
|
await NativeVaultManager.setAuthMethods(methodsToSave);
|
|
} catch (error) {
|
|
console.error('Failed to update native auth methods:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable an authentication method by adding it to the current auth methods.
|
|
* This handles getting current methods, adding the new one, and saving.
|
|
*/
|
|
static async enableAuthMethod(method: AuthMethod): Promise<void> {
|
|
try {
|
|
const currentMethods = await this.getEnabledAuthMethods();
|
|
if (!currentMethods.includes(method)) {
|
|
await this.setAuthMethods([...currentMethods, method]);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to enable auth method ${method}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable an authentication method by removing it from the current auth methods.
|
|
* This handles getting current methods, removing the specified one, and saving.
|
|
* Password cannot be disabled as it's always required as a fallback.
|
|
*/
|
|
static async disableAuthMethod(method: AuthMethod): Promise<void> {
|
|
if (method === 'password') {
|
|
console.warn('Cannot disable password auth method - it is always required');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const currentMethods = await this.getEnabledAuthMethods();
|
|
const updatedMethods = currentMethods.filter(m => m !== method);
|
|
await this.setAuthMethods(updatedMethods);
|
|
} catch (error) {
|
|
console.error(`Failed to disable auth method ${method}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Check if biometrics are available on the device (hardware + enrollment).
|
|
* This only checks device capabilities, not key validity.
|
|
*/
|
|
static async isBiometricsAvailableOnDevice(): Promise<boolean> {
|
|
try {
|
|
const hasHardware = await LocalAuthentication.hasHardwareAsync();
|
|
if (!hasHardware) {
|
|
return false;
|
|
}
|
|
|
|
return await LocalAuthentication.isEnrolledAsync();
|
|
} catch (error) {
|
|
console.error('Error checking biometric device availability:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if biometric unlock is actually available and functional.
|
|
* This performs comprehensive validation:
|
|
* - Device has biometric hardware
|
|
* - Biometrics are enrolled
|
|
* - Biometrics are enabled in auth methods
|
|
* - Encryption key in native KeyStore/Keychain is valid
|
|
*
|
|
* Returns false if the key has been invalidated (e.g., biometric enrollment changed).
|
|
* Use this method when determining whether to show biometric unlock UI.
|
|
*
|
|
* IMPORTANT: If the key is invalid but 'faceid' is still in auth methods,
|
|
* this method will automatically remove it to keep state consistent.
|
|
*/
|
|
static async isBiometricUnlockAvailable(): Promise<boolean> {
|
|
try {
|
|
// First check device capabilities
|
|
const deviceAvailable = await this.isBiometricsAvailableOnDevice();
|
|
if (!deviceAvailable) {
|
|
return false;
|
|
}
|
|
|
|
// Then validate that biometric unlock is actually functional
|
|
// This checks auth methods AND validates the encryption key
|
|
const isAvailable = await NativeVaultManager.isBiometricUnlockAvailable();
|
|
|
|
// If biometric unlock is NOT available but 'faceid' is still in auth methods,
|
|
// automatically remove it to keep state consistent
|
|
if (!isAvailable) {
|
|
const currentMethods = await this.getEnabledAuthMethods();
|
|
if (currentMethods.includes('faceid')) {
|
|
console.log('Biometric key invalid but faceid still in auth methods - cleaning up');
|
|
const methodsWithoutBiometric = currentMethods.filter(m => m !== 'faceid');
|
|
await this.setAuthMethods(methodsWithoutBiometric);
|
|
}
|
|
}
|
|
|
|
return isAvailable;
|
|
} catch (error) {
|
|
console.error('Error checking biometric unlock availability:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate biometric display name translation key based on device capabilities.
|
|
* Returns localization keys like 'settings.vaultUnlockSettings.faceId', 'biometrics', etc.
|
|
*/
|
|
static async getBiometricDisplayName(): Promise<string> {
|
|
try {
|
|
const hasBiometrics = await LocalAuthentication.hasHardwareAsync();
|
|
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
|
|
|
// For Android, we use the term "Biometrics" for facial recognition and fingerprint.
|
|
if (Platform.OS === 'android') {
|
|
return i18n.t('settings.vaultUnlockSettings.biometrics');
|
|
}
|
|
|
|
// For iOS, we check if the device has explicit Face ID or Touch ID support.
|
|
if (!hasBiometrics || !enrolled) {
|
|
return i18n.t('settings.vaultUnlockSettings.faceIdTouchId');
|
|
}
|
|
|
|
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
|
|
const hasFaceIDSupport = types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION);
|
|
const hasTouchIDSupport = types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT);
|
|
|
|
if (hasFaceIDSupport) {
|
|
return i18n.t('settings.vaultUnlockSettings.faceId');
|
|
} else if (hasTouchIDSupport) {
|
|
return i18n.t('settings.vaultUnlockSettings.touchId');
|
|
}
|
|
|
|
return i18n.t('settings.vaultUnlockSettings.faceIdTouchId');
|
|
} catch (error) {
|
|
console.error('Failed to get biometric display name:', error);
|
|
return i18n.t('settings.vaultUnlockSettings.faceIdTouchId');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the display label translation key for the current auth method.
|
|
* Priority: Biometrics > PIN > Password
|
|
*
|
|
* @returns Translation key for the primary unlock method
|
|
*/
|
|
static async getAuthMethodDisplayKey(): Promise<string> {
|
|
try {
|
|
// Check for biometrics first (highest priority)
|
|
const methods = await this.getEnabledAuthMethods();
|
|
if (methods.includes('faceid')) {
|
|
if (await this.isBiometricUnlockAvailable()) {
|
|
return await this.getBiometricDisplayName();
|
|
}
|
|
}
|
|
|
|
// Check for PIN (second priority)
|
|
const pinEnabled = await NativeVaultManager.isPinEnabled();
|
|
if (pinEnabled) {
|
|
return 'settings.vaultUnlockSettings.pin';
|
|
}
|
|
|
|
// Fallback to password
|
|
return 'items.password';
|
|
} catch (error) {
|
|
console.error('Failed to get auth method display key:', error);
|
|
return 'items.password';
|
|
}
|
|
}
|
|
}
|