diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt index 0b4f5279a..c4f0a8316 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultStore.kt @@ -25,6 +25,7 @@ class VaultStore( ) { private var dbConnection: SQLiteDatabase? = null private val TAG = "VaultStore" + private val BIOMETRICS_AUTH_METHOD = "faceid" private var isVaultUnlocked = false private var autoLockTimeout: Long = 300000 // 5 minutes default private var lastUnlockTime: Long = 0 @@ -41,7 +42,7 @@ class VaultStore( // Check if biometric auth is enabled in auth methods val authMethods = getAuthMethods() - if (authMethods.contains("biometric") && keystoreProvider.isBiometricAvailable()) { + if (authMethods.contains(BIOMETRICS_AUTH_METHOD) && keystoreProvider.isBiometricAvailable()) { keystoreProvider.storeKey( key = base64EncryptionKey, object : KeystoreOperationCallback { @@ -67,7 +68,9 @@ class VaultStore( // Check if biometric auth is enabled in auth methods val authMethods = getAuthMethods() - if (authMethods.contains("biometric") && keystoreProvider.isBiometricAvailable()) { + val contains = authMethods.contains(BIOMETRICS_AUTH_METHOD) + val available = keystoreProvider.isBiometricAvailable() + if (authMethods.contains(BIOMETRICS_AUTH_METHOD) && keystoreProvider.isBiometricAvailable()) { keystoreProvider.retrieveKey( object : KeystoreOperationCallback { override fun onSuccess(result: String) { diff --git a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx index e582af604..3703bb1ea 100644 --- a/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx +++ b/apps/mobile-app/app/(tabs)/settings/vault-unlock.tsx @@ -16,9 +16,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { const colors = useColors(); const [initialized, setInitialized] = useState(false); const { setAuthMethods, getEnabledAuthMethods, getBiometricDisplayName } = useAuth(); - const [hasFaceID, setHasFaceID] = useState(false); - const [isFaceIDEnabled, setIsFaceIDEnabled] = useState(false); - const [biometricDisplayName, setBiometricDisplayName] = useState('Face ID / Touch ID'); + const [hasBiometrics, setHasBiometrics] = useState(false); + const [isBiometricsEnabled, setIsBiometricsEnabled] = useState(false); + const [biometricDisplayName, setBiometricDisplayName] = useState(Platform.OS === 'ios' ? 'Face ID / Touch ID' : 'Biometrics'); const [_, setEnabledAuthMethods] = useState([]); useEffect(() => { @@ -26,21 +26,42 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { * Initialize the auth methods. */ const initializeAuth = async () : Promise => { - const compatible = await LocalAuthentication.hasHardwareAsync(); - const enrolled = await LocalAuthentication.isEnrolledAsync(); - setHasFaceID(compatible && enrolled); + try { + // Check for hardware support + const compatible = await LocalAuthentication.hasHardwareAsync(); - const displayName = await getBiometricDisplayName(); - setBiometricDisplayName(displayName); + // Check if any biometrics are enrolled + const enrolled = await LocalAuthentication.isEnrolledAsync(); - const methods = await getEnabledAuthMethods(); - setEnabledAuthMethods(methods); + // Check for strong authentication support + const hasStrongAuth = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Checking biometric capabilities', + disableDeviceFallback: true, + cancelLabel: 'Cancel', + fallbackLabel: 'Use password', + }).then(result => result.success); - if (methods.includes('faceid') && enrolled) { - setIsFaceIDEnabled(true); + // Set biometric availability based on all checks + const isBiometricAvailable = compatible && enrolled && hasStrongAuth; + setHasBiometrics(isBiometricAvailable); + + // Get appropriate display name + const displayName = Platform.OS === 'ios' ? await getBiometricDisplayName() : 'Biometrics'; + setBiometricDisplayName(displayName); + + const methods = await getEnabledAuthMethods(); + setEnabledAuthMethods(methods); + + if (methods.includes('faceid') && enrolled) { + setIsBiometricsEnabled(true); + } + + setInitialized(true); + } catch (error) { + console.error('Failed to initialize auth:', error); + setHasBiometrics(false); + setInitialized(true); } - - setInitialized(true); }; initializeAuth(); @@ -56,7 +77,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { */ const updateAuthMethods = async () : Promise => { const currentAuthMethods = await getEnabledAuthMethods(); - const newAuthMethods = isFaceIDEnabled ? ['faceid', 'password'] : ['password']; + const newAuthMethods = isBiometricsEnabled ? ['faceid', 'password'] : ['password']; if (currentAuthMethods.length === newAuthMethods.length && currentAuthMethods.every(method => newAuthMethods.includes(method))) { @@ -67,13 +88,13 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { }; updateAuthMethods(); - }, [isFaceIDEnabled, setAuthMethods, getEnabledAuthMethods, initialized]); + }, [isBiometricsEnabled, setAuthMethods, getEnabledAuthMethods, initialized]); - const handleFaceIDToggle = useCallback(async (value: boolean) : Promise => { - if (value && !hasFaceID) { + const handleBiometricsToggle = useCallback(async (value: boolean) : Promise => { + if (value && !hasBiometrics) { Alert.alert( - 'Face ID Not Available', - 'Face ID is disabled for AliasVault. In order to use it, please enable it in the iOS app settings first.', + `${biometricDisplayName} Not Available`, + `${biometricDisplayName} is disabled for AliasVault. In order to use it, please enable it in your device settings first.`, [ { text: 'Open Settings', @@ -81,10 +102,12 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { * Handle the open settings press. */ onPress: () : void => { - setIsFaceIDEnabled(true); + setIsBiometricsEnabled(true); setAuthMethods(['faceid', 'password']); if (Platform.OS === 'ios') { Linking.openURL('app-settings:'); + } else { + Linking.openSettings(); } }, }, @@ -95,7 +118,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { * Handle the cancel press. */ onPress: () : void => { - setIsFaceIDEnabled(false); + setIsBiometricsEnabled(false); setAuthMethods(['password']); }, }, @@ -104,19 +127,19 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { return; } - setIsFaceIDEnabled(value); + setIsBiometricsEnabled(value); setAuthMethods(value ? ['faceid', 'password'] : ['password']); - // Show toast notification only on Face ID enabled + // Show toast notification only on biometrics enabled if (value) { Toast.show({ type: 'success', - text1: 'Face ID is now successfully enabled', + text1: `${biometricDisplayName} is now successfully enabled`, position: 'bottom', visibilityTime: 1200, }); } - }, [hasFaceID, setAuthMethods]); + }, [hasBiometrics, setAuthMethods, biometricDisplayName]); const styles = StyleSheet.create({ container: { @@ -177,25 +200,25 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode { handleFaceIDToggle(!isFaceIDEnabled)} + onPress={() => handleBiometricsToggle(!isBiometricsEnabled)} > - + {biometricDisplayName} - Your vault decryption key will be securely stored on your local device in the iOS Keychain and can be accessed securely with {biometricDisplayName}. + Your vault decryption key will be securely stored on your local device in the {Platform.OS === 'ios' ? 'iOS Keychain' : 'Android Keystore'} and can be accessed securely with {biometricDisplayName}. - {!hasFaceID && ( + {!hasBiometrics && ( - {biometricDisplayName} is blocked in iOS settings. Tap to open settings and enable it. + {biometricDisplayName} is blocked in device settings. Tap to open settings and enable it. )}