Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REF: UnlockWith handle all scenarios of auth #6169

Merged
merged 4 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ const ScanQRCodeRoot = () => (
const UnlockWithScreenStack = createNativeStackNavigator();
const UnlockWithScreenRoot = () => (
<UnlockWithScreenStack.Navigator name="UnlockWithScreenRoot" screenOptions={{ headerShown: false, statusBarStyle: 'auto' }}>
<UnlockWithScreenStack.Screen name="UnlockWithScreen" component={UnlockWith} initialParams={{ unlockOnComponentMount: true }} />
<UnlockWithScreenStack.Screen name="UnlockWithScreen" component={UnlockWith} />
</UnlockWithScreenStack.Navigator>
);

Expand Down
9 changes: 9 additions & 0 deletions NavigationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ export function dispatch(action: NavigationAction) {
navigationRef.current?.dispatch(action);
}
}

export function reset() {
if (navigationRef.isReady()) {
navigationRef.current?.reset({
index: 0,
routes: [{ name: 'UnlockWithScreenRoot' }],
});
}
}
148 changes: 77 additions & 71 deletions UnlockWith.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
import React, { useContext, useEffect, useReducer, useRef } from 'react';
import React, { useCallback, useContext, useEffect, useReducer, useRef } from 'react';
import { View, Image, TouchableOpacity, ActivityIndicator, useColorScheme, NativeModules, StyleSheet } from 'react-native';
import { Icon } from 'react-native-elements';
import Biometric, { BiometricType } from './class/biometrics';
import { NavigationProp, RouteProp, StackActions, useNavigation, useRoute } from '@react-navigation/native';
import { StackActions, useNavigation } from '@react-navigation/native';
import { BlueStorageContext } from './blue_modules/storage-context';
import { isHandset } from './blue_modules/environment';
import triggerHapticFeedback, { HapticFeedbackTypes } from './blue_modules/hapticFeedback';
import SafeArea from './components/SafeArea';
type RootStackParamList = {
UnlockWith: { unlockOnComponentMount?: boolean };
};

enum AuthType {
Encrypted,
Biometrics,
None,
}

type State = {
biometricType: keyof typeof BiometricType | undefined;
isStorageEncryptedEnabled: boolean;
auth: {
type: AuthType;
detail: keyof typeof BiometricType | undefined;
};
isAuthenticating: boolean;
};

const SET_BIOMETRIC_TYPE = 'SET_BIOMETRIC_TYPE';
const SET_IS_STORAGE_ENCRYPTED_ENABLED = 'SET_IS_STORAGE_ENCRYPTED_ENABLED';
const SET_AUTH = 'SET_AUTH';
const SET_IS_AUTHENTICATING = 'SET_IS_AUTHENTICATING';

type Action =
| { type: typeof SET_BIOMETRIC_TYPE; payload: keyof typeof BiometricType | undefined }
| { type: typeof SET_IS_STORAGE_ENCRYPTED_ENABLED; payload: boolean }
| { type: typeof SET_AUTH; payload: { type: AuthType; detail: keyof typeof BiometricType | undefined } }
| { type: typeof SET_IS_AUTHENTICATING; payload: boolean };

const initialState: State = {
biometricType: undefined,
isStorageEncryptedEnabled: false,
auth: {
type: AuthType.None,
detail: undefined,
},
isAuthenticating: false,
};

function reducer(state: State, action: Action): State {
switch (action.type) {
case SET_BIOMETRIC_TYPE:
return { ...state, biometricType: action.payload };
case SET_IS_STORAGE_ENCRYPTED_ENABLED:
return { ...state, isStorageEncryptedEnabled: action.payload };
case SET_AUTH:
return { ...state, auth: action.payload };
case SET_IS_AUTHENTICATING:
return { ...state, isAuthenticating: action.payload };
default:
Expand All @@ -51,24 +54,16 @@ const UnlockWith: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const isUnlockingWallets = useRef(false);
const { setWalletsInitialized, isStorageEncrypted, startAndDecrypt } = useContext(BlueStorageContext);
const navigation = useNavigation<NavigationProp<RootStackParamList, 'UnlockWith'>>();
const route = useRoute<RouteProp<RootStackParamList, 'UnlockWith'>>();
const { unlockOnComponentMount } = route.params;
const navigation = useNavigation();
const colorScheme = useColorScheme();

useEffect(() => {
SplashScreen?.dismissSplashScreen();
startUnlock();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const successfullyAuthenticated = () => {
const successfullyAuthenticated = useCallback(() => {
setWalletsInitialized(true);
navigation.dispatch(StackActions.replace(isHandset ? 'Navigation' : 'DrawerRoot'));
isUnlockingWallets.current = false;
};
}, [setWalletsInitialized, navigation]);

const unlockWithBiometrics = async () => {
const unlockWithBiometrics = useCallback(async () => {
if (isUnlockingWallets.current || state.isAuthenticating) return;
isUnlockingWallets.current = true;
dispatch({ type: SET_IS_AUTHENTICATING, payload: true });
Expand All @@ -80,9 +75,9 @@ const UnlockWith: React.FC = () => {

dispatch({ type: SET_IS_AUTHENTICATING, payload: false });
isUnlockingWallets.current = false;
};
}, [state.isAuthenticating, startAndDecrypt, successfullyAuthenticated]);

const unlockWithKey = async () => {
const unlockWithKey = useCallback(async () => {
if (isUnlockingWallets.current || state.isAuthenticating) return;
isUnlockingWallets.current = true;
dispatch({ type: SET_IS_AUTHENTICATING, payload: true });
Expand All @@ -94,53 +89,64 @@ const UnlockWith: React.FC = () => {
dispatch({ type: SET_IS_AUTHENTICATING, payload: false });
isUnlockingWallets.current = false;
}
};
}, [state.isAuthenticating, startAndDecrypt, successfullyAuthenticated]);

const renderUnlockOptions = () => {
if (state.isAuthenticating) {
return <ActivityIndicator />;
} else {
const color = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
if (
(state.biometricType === BiometricType.TouchID || state.biometricType === BiometricType.Biometrics) &&
!state.isStorageEncryptedEnabled
) {
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithBiometrics}>
<Icon name="fingerprint" size={64} type="font-awesome5" color={color} />
</TouchableOpacity>
);
} else if (state.biometricType === BiometricType.FaceID && !state.isStorageEncryptedEnabled) {
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithBiometrics}>
<Image
source={colorScheme === 'dark' ? require('./img/faceid-default.png') : require('./img/faceid-dark.png')}
style={styles.icon}
/>
</TouchableOpacity>
);
} else if (state.isStorageEncryptedEnabled) {
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithKey}>
<Icon name="lock" size={64} type="font-awesome5" color={color} />
</TouchableOpacity>
);
}
}
};
useEffect(() => {
SplashScreen?.dismissSplashScreen();

const startUnlock = async () => {
if (unlockOnComponentMount) {
const startUnlock = async () => {
const storageIsEncrypted = await isStorageEncrypted();
const isBiometricUseCapableAndEnabled = await Biometric.isBiometricUseCapableAndEnabled();
const rawType = isBiometricUseCapableAndEnabled ? await Biometric.biometricType() : undefined;
const biometricType = isBiometricUseCapableAndEnabled ? await Biometric.biometricType() : undefined;

if (!rawType || storageIsEncrypted) {
dispatch({ type: SET_IS_STORAGE_ENCRYPTED_ENABLED, payload: storageIsEncrypted });
if (storageIsEncrypted) {
dispatch({ type: SET_AUTH, payload: { type: AuthType.Encrypted, detail: undefined } });
unlockWithKey();
} else {
dispatch({ type: SET_BIOMETRIC_TYPE, payload: rawType });
} else if (isBiometricUseCapableAndEnabled) {
dispatch({ type: SET_AUTH, payload: { type: AuthType.Biometrics, detail: biometricType } });
unlockWithBiometrics();
} else {
dispatch({ type: SET_AUTH, payload: { type: AuthType.None, detail: undefined } });
unlockWithKey();
}
};

startUnlock();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const renderUnlockOptions = () => {
const color = colorScheme === 'dark' ? '#FFFFFF' : '#000000';
if (state.isAuthenticating) {
return <ActivityIndicator />;
} else {
switch (state.auth.type) {
case AuthType.Biometrics:
if (state.auth.detail === 'TouchID' || state.auth.detail === 'Biometrics') {
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithBiometrics}>
<Icon name="fingerprint" size={64} type="font-awesome5" color={color} />
</TouchableOpacity>
);
} else if (state.auth.detail === 'FaceID') {
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithBiometrics}>
<Image
source={colorScheme === 'dark' ? require('./img/faceid-default.png') : require('./img/faceid-dark.png')}
style={styles.icon}
/>
</TouchableOpacity>
);
}
return null;
case AuthType.Encrypted:
return (
<TouchableOpacity accessibilityRole="button" disabled={state.isAuthenticating} onPress={unlockWithKey}>
<Icon name="lock" size={64} type="font-awesome5" color={color} />
</TouchableOpacity>
);
default:
return null;
}
}
};
Expand Down
36 changes: 22 additions & 14 deletions class/biometrics.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useContext } from 'react';
import { Alert, Platform } from 'react-native';
import { CommonActions, StackActions } from '@react-navigation/native';
import ReactNativeBiometrics, { BiometryTypes as RNBiometryTypes } from 'react-native-biometrics';
import PasscodeAuth from 'react-native-passcode-auth';
import RNSecureKeyStore from 'react-native-secure-key-store';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import loc from '../loc';
import * as NavigationService from '../NavigationService';
import { BlueStorageContext } from '../blue_modules/storage-context';
Expand Down Expand Up @@ -83,7 +82,6 @@ const Biometric = function () {

Biometric.unlockWithBiometrics = async () => {
const isDeviceBiometricCapable = await Biometric.isDeviceBiometricCapable();
console.warn('Biometric.unlockWithBiometrics isDeviceBiometricCapable unlockWithBiometrics', isDeviceBiometricCapable);
if (isDeviceBiometricCapable) {
return new Promise(resolve => {
rnBiometrics
Expand All @@ -107,10 +105,24 @@ const Biometric = function () {
};

const clearKeychain = async () => {
await RNSecureKeyStore.remove('data');
await RNSecureKeyStore.remove('data_encrypted');
await RNSecureKeyStore.remove(STORAGEKEY);
NavigationService.dispatch(StackActions.replace('WalletsRoot'));
try {
console.log('Wiping keychain');
console.log('Wiping key: data');
await RNSecureKeyStore.set('data', JSON.stringify({ data: { wallets: [] } }), {
accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
console.log('Wiped key: data');
console.log('Wiping key: data_encrypted');
await RNSecureKeyStore.set('data_encrypted', '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
console.log('Wiped key: data_encrypted');
console.log('Wiping key: STORAGEKEY');
await RNSecureKeyStore.set(STORAGEKEY, '', { accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY });
console.log('Wiped key: STORAGEKEY');
NavigationService.reset();
} catch (error: any) {
console.warn(error);
presentAlert({ message: error.message });
}
};

const requestDevicePasscode = async () => {
Expand All @@ -127,7 +139,8 @@ const Biometric = function () {
{ text: loc._.cancel, style: 'cancel' },
{
text: loc._.ok,
onPress: () => clearKeychain(),
style: 'destructive',
onPress: async () => await clearKeychain(),
},
],
{ cancelable: false },
Expand All @@ -151,12 +164,7 @@ const Biometric = function () {
{
text: loc._.cancel,
onPress: () => {
NavigationService.dispatch(
CommonActions.setParams({
index: 0,
routes: [{ name: 'UnlockWithScreenRoot' }, { params: { unlockOnComponentMount: false } }],
}),
);
console.log('Cancel Pressed');
},
style: 'cancel',
},
Expand Down
10 changes: 5 additions & 5 deletions ios/BlueWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
C59F90CE0D04D3E4BB39BC5D /* libPods-BlueWalletUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F02C2F7CA3591E4E0B06EBA /* libPods-BlueWalletUITests.a */; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
E5D4794B26781FC0007838C1 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = 6DD410AD266CAF1F0087DE03 /* fiatUnits.json */; };
E5D4794C26781FC1007838C1 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = 6DD410AD266CAF1F0087DE03 /* fiatUnits.json */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -399,7 +399,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
773E382FE62E836172AAB98B /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1024,7 +1024,7 @@
);
mainGroup = 83CBB9F61A601CBA00E9B192;
packageReferences = (
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */,
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
);
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
Expand Down Expand Up @@ -2518,7 +2518,7 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */ = {
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/EFPrefix/EFQRCode.git";
requirement = {
Expand All @@ -2539,7 +2539,7 @@
/* Begin XCSwiftPackageProductDependency section */
6DFC806F24EA0B6C007B8700 /* EFQRCode */ = {
isa = XCSwiftPackageProductDependency;
package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */;
package = 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode.git" */;
productName = EFQRCode;
};
B41B76842B66B2FF002C48D5 /* Bugsnag */ = {
Expand Down
8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -473,15 +473,15 @@ PODS:
- React
- rn-ldk (0.8.4):
- React-Core
- RNCAsyncStorage (1.22.1):
- RNCAsyncStorage (1.22.2):
- React-Core
- RNCClipboard (1.13.2):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (10.12.0):
- RNDeviceInfo (10.13.0):
- React-Core
- RNFS (2.20.0):
- React-Core
Expand Down Expand Up @@ -845,11 +845,11 @@ SPEC CHECKSUMS:
ReactNativeCameraKit: 9d46a5d7dd544ca64aa9c03c150d2348faf437eb
RealmJS: a62dc7a1f94b888fe9e8712cd650167ad97dc636
rn-ldk: 0d8749d98cc5ce67302a32831818c116b67f7643
RNCAsyncStorage: 7deab901e27d1f989a83e8be6ce91b673772c848
RNCAsyncStorage: 014a78b2cc8cc107c9e92ee428dc0c1ac3223416
RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120
RNDeviceInfo: 5e4695f906aeb624855cee8d568a46037e4a50f7
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 7909c50383a18f0cb10ce1db7262b9a6da504c03
RNHandoff: d3b0754cca3a6bcd9b25f544f733f7f033ccf5fa
Expand Down