import * as LocalAuthentication from 'expo-local-authentication'; import moment from 'moment'; import RNBootSplash from 'react-native-bootsplash'; import AsyncStorage from '@react-native-community/async-storage'; import { sha256 } from 'js-sha256'; import UserPreferences from '../lib/userPreferences'; import store from '../lib/createStore'; import database from '../lib/database'; import { isIOS } from './deviceInfo'; import EventEmitter from './events'; import { LOCAL_AUTHENTICATE_EMITTER, LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY, PASSCODE_KEY, CHANGE_PASSCODE_EMITTER } from '../constants/localAuthentication'; import I18n from '../i18n'; import { setLocalAuthenticated } from '../actions/login'; export const saveLastLocalAuthenticationSession = async(server, serverRecord) => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); await serversDB.action(async() => { try { if (!serverRecord) { serverRecord = await serversCollection.find(server); } await serverRecord.update((record) => { record.lastLocalAuthenticatedSession = new Date(); }); } catch (e) { // Do nothing } }); }; export const resetAttempts = () => AsyncStorage.multiRemove([LOCKED_OUT_TIMER_KEY, ATTEMPTS_KEY]); const openModal = hasBiometry => new Promise((resolve) => { EventEmitter.emit(LOCAL_AUTHENTICATE_EMITTER, { submit: () => resolve(), hasBiometry }); }); const openChangePasscodeModal = ({ force }) => new Promise((resolve, reject) => { EventEmitter.emit(CHANGE_PASSCODE_EMITTER, { submit: passcode => resolve(passcode), cancel: () => reject(), force }); }); export const changePasscode = async({ force = false }) => { const passcode = await openChangePasscodeModal({ force }); await UserPreferences.setStringAsync(PASSCODE_KEY, sha256(passcode)); }; export const biometryAuth = force => LocalAuthentication.authenticateAsync({ disableDeviceFallback: true, cancelLabel: force ? I18n.t('Dont_activate') : I18n.t('Local_authentication_biometry_fallback'), promptMessage: I18n.t('Local_authentication_biometry_title') }); /* * It'll help us to get the permission to use FaceID * and enable/disable the biometry when user put their first passcode */ const checkBiometry = async(serverRecord) => { const serversDB = database.servers; const result = await biometryAuth(true); await serversDB.action(async() => { try { await serverRecord.update((record) => { record.biometry = !!result?.success; }); } catch { // Do nothing } }); }; export const checkHasPasscode = async({ force = true, serverRecord }) => { const storedPasscode = await UserPreferences.getStringAsync(PASSCODE_KEY); if (!storedPasscode) { await changePasscode({ force }); await checkBiometry(serverRecord); return Promise.resolve({ newPasscode: true }); } return Promise.resolve(); }; export const localAuthenticate = async(server) => { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); let serverRecord; try { serverRecord = await serversCollection.find(server); } catch (error) { return Promise.reject(); } // if screen lock is enabled if (serverRecord?.autoLock) { // Make sure splash screen has been hidden await RNBootSplash.hide(); // Check if the app has passcode const result = await checkHasPasscode({ serverRecord }); // `checkHasPasscode` results newPasscode = true if a passcode has been set if (!result?.newPasscode) { // diff to last authenticated session const diffToLastSession = moment().diff(serverRecord?.lastLocalAuthenticatedSession, 'seconds'); // if last authenticated session is older than configured auto lock time, authentication is required if (diffToLastSession >= serverRecord?.autoLockTime) { // set isLocalAuthenticated to false store.dispatch(setLocalAuthenticated(false)); let hasBiometry = false; // if biometry is enabled on the app if (serverRecord.biometry) { const isEnrolled = await LocalAuthentication.isEnrolledAsync(); hasBiometry = isEnrolled; } // Authenticate await openModal(hasBiometry); // set isLocalAuthenticated to true store.dispatch(setLocalAuthenticated(true)); } } await resetAttempts(); await saveLastLocalAuthenticationSession(server, serverRecord); } }; export const supportedBiometryLabel = async() => { try { const enrolled = await LocalAuthentication.isEnrolledAsync(); if (!enrolled) { return null; } const supported = await LocalAuthentication.supportedAuthenticationTypesAsync(); if (supported.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) { return isIOS ? 'FaceID' : I18n.t('Local_authentication_facial_recognition'); } if (supported.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) { return isIOS ? 'TouchID' : I18n.t('Local_authentication_fingerprint'); } } catch { // Do nothing } return null; };