1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Mobile: Add support for locking the app using biometrics

This commit is contained in:
Laurent Cozic 2023-01-04 20:18:51 +00:00
parent 5a05cc5797
commit f10d9f75b0
12 changed files with 194 additions and 0 deletions

View File

@ -990,6 +990,12 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/biometrics/BiometricPopup.d.ts
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/BiometricPopup.js.map
packages/app-mobile/components/biometrics/sensorInfo.d.ts
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/biometrics/sensorInfo.js.map
packages/app-mobile/components/getResponsiveValue.d.ts packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map packages/app-mobile/components/getResponsiveValue.js.map

6
.gitignore vendored
View File

@ -978,6 +978,12 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map packages/app-mobile/components/SideMenu.js.map
packages/app-mobile/components/biometrics/BiometricPopup.d.ts
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/BiometricPopup.js.map
packages/app-mobile/components/biometrics/sensorInfo.d.ts
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/biometrics/sensorInfo.js.map
packages/app-mobile/components/getResponsiveValue.d.ts packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map packages/app-mobile/components/getResponsiveValue.js.map

View File

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- Make these features optional to enable Chromebooks --> <!-- Make these features optional to enable Chromebooks -->
<!-- https://github.com/laurent22/joplin/issues/37 --> <!-- https://github.com/laurent22/joplin/issues/37 -->

View File

@ -46,6 +46,11 @@ allprojects {
excludeGroup "com.facebook.react" excludeGroup "com.facebook.react"
} }
} }
maven {
// Required by react-native-fingerprint-scanner
// https://github.com/hieuvp/react-native-fingerprint-scanner/issues/192
url "https://maven.aliyun.com/repository/jcenter"
}
google() google()
maven { url 'https://www.jitpack.io' } maven { url 'https://www.jitpack.io' }
} }

View File

@ -0,0 +1,89 @@
const React = require('react');
import Setting from '@joplin/lib/models/Setting';
import { useEffect, useMemo, useState } from 'react';
import { View, Dimensions, Alert, Button } from 'react-native';
import FingerprintScanner from 'react-native-fingerprint-scanner';
import { SensorInfo } from './sensorInfo';
import { _ } from '@joplin/lib/locale';
interface Props {
themeId: number;
sensorInfo: SensorInfo;
}
export default (props: Props) => {
const [initialPromptDone, setInitialPromptDone] = useState(false); // Setting.value('security.biometricsInitialPromptDone'));
const [display, setDisplay] = useState(!!props.sensorInfo.supportedSensors && (props.sensorInfo.enabled || !initialPromptDone));
const [tryBiometricsCheck, setTryBiometricsCheck] = useState(initialPromptDone);
useEffect(() => {
if (!display || !tryBiometricsCheck) return;
const biometricsCheck = async () => {
try {
await FingerprintScanner.authenticate({ description: _('Verify your identity') });
setTryBiometricsCheck(false);
setDisplay(false);
} catch (error) {
Alert.alert(_('Could not verify your identify'), error.message);
setTryBiometricsCheck(false);
} finally {
FingerprintScanner.release();
}
};
void biometricsCheck();
}, [display, tryBiometricsCheck]);
useEffect(() => {
if (initialPromptDone) return;
if (!display) return;
const complete = (enableBiometrics: boolean) => {
setInitialPromptDone(true);
Setting.setValue('security.biometricsInitialPromptDone', true);
Setting.setValue('security.biometricsEnabled', enableBiometrics);
if (!enableBiometrics) {
setDisplay(false);
setTryBiometricsCheck(false);
} else {
setTryBiometricsCheck(true);
}
};
Alert.alert(
_('Enable biometrics authentication?'),
_('Use your biometrics to secure access to your application. You can always set it up later in Settings.'),
[
{
text: _('Enable'),
onPress: () => complete(true),
style: 'default',
},
{
text: _('Not now'),
onPress: () => complete(false),
style: 'cancel',
},
]
);
}, [initialPromptDone, props.sensorInfo.supportedSensors, display]);
const windowSize = useMemo(() => {
return {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
};
}, []);
const renderTryAgainButton = () => {
if (!display || tryBiometricsCheck || !initialPromptDone) return null;
return <Button title={_('Try again')} onPress={() => setTryBiometricsCheck(true)} />;
};
return (
<View style={{ display: display ? 'flex' : 'none', position: 'absolute', zIndex: 99999, backgroundColor: '#000000', width: windowSize.width, height: windowSize.height }}>
{renderTryAgainButton()}
</View>
);
};

View File

@ -0,0 +1,37 @@
import Setting from '@joplin/lib/models/Setting';
import FingerprintScanner from 'react-native-fingerprint-scanner';
export interface SensorInfo {
enabled: boolean;
sensorsHaveChanged: boolean;
supportedSensors: string;
}
export default async (): Promise<SensorInfo> => {
const enabled = Setting.value('security.biometricsEnabled');
let hasChanged = false;
let supportedSensors = '';
if (enabled) {
try {
const result = await FingerprintScanner.isSensorAvailable();
supportedSensors = result;
if (result) {
if (result !== Setting.value('security.biometricsSupportedSensors')) {
hasChanged = true;
Setting.setValue('security.biometricsSupportedSensors', result);
}
}
} catch (error) {
console.warn('Could not check for biometrics sensor:', error);
Setting.setValue('security.biometricsSupportedSensors', '');
}
}
return {
enabled,
sensorsHaveChanged: hasChanged,
supportedSensors,
};
};

View File

@ -107,5 +107,7 @@
<string>Light</string> <string>Light</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSFaceIDUsageDescription</key>
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
</dict> </dict>
</plist> </plist>

View File

@ -234,6 +234,8 @@ PODS:
- React-Core - React-Core
- react-native-document-picker (8.1.3): - react-native-document-picker (8.1.3):
- React-Core - React-Core
- react-native-fingerprint-scanner (6.0.0):
- React
- react-native-geolocation (2.1.0): - react-native-geolocation (2.1.0):
- React-Core - React-Core
- react-native-get-random-values (1.8.0): - react-native-get-random-values (1.8.0):
@ -367,6 +369,7 @@ DEPENDENCIES:
- react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`) - react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
- react-native-camera (from `../node_modules/react-native-camera`) - react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`) - react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`)
@ -452,6 +455,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-camera" :path: "../node_modules/react-native-camera"
react-native-document-picker: react-native-document-picker:
:path: "../node_modules/react-native-document-picker" :path: "../node_modules/react-native-document-picker"
react-native-fingerprint-scanner:
:path: "../node_modules/react-native-fingerprint-scanner"
react-native-geolocation: react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation" :path: "../node_modules/@react-native-community/geolocation"
react-native-get-random-values: react-native-get-random-values:
@ -544,6 +549,7 @@ SPEC CHECKSUMS:
react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52 react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695 react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695

View File

@ -45,6 +45,7 @@
"react-native-document-picker": "8.1.3", "react-native-document-picker": "8.1.3",
"react-native-dropdownalert": "4.5.1", "react-native-dropdownalert": "4.5.1",
"react-native-file-viewer": "2.1.5", "react-native-file-viewer": "2.1.5",
"react-native-fingerprint-scanner": "6.0.0",
"react-native-fs": "2.20.0", "react-native-fs": "2.20.0",
"react-native-get-random-values": "1.8.0", "react-native-get-random-values": "1.8.0",
"react-native-image-picker": "4.10.3", "react-native-image-picker": "4.10.3",

View File

@ -83,6 +83,7 @@ const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js'); const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js'); const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js'); const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import BiometricPopup from './components/biometrics/BiometricPopup';
SyncTargetRegistry.addClass(SyncTargetNone); SyncTargetRegistry.addClass(SyncTargetNone);
SyncTargetRegistry.addClass(SyncTargetOneDrive); SyncTargetRegistry.addClass(SyncTargetOneDrive);
@ -108,6 +109,7 @@ import { setRSA } from '@joplin/lib/services/e2ee/ppk';
import RSA from './services/e2ee/RSA.react-native'; import RSA from './services/e2ee/RSA.react-native';
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils'; import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { AppState } from './utils/types'; import { AppState } from './utils/types';
import sensorInfo from './components/biometrics/sensorInfo';
let storeDispatch = function(_action: any) {}; let storeDispatch = function(_action: any) {};
@ -690,6 +692,7 @@ class AppComponent extends React.Component {
this.state = { this.state = {
sideMenuContentOpacity: new Animated.Value(0), sideMenuContentOpacity: new Animated.Value(0),
sideMenuWidth: this.getSideMenuWidth(), sideMenuWidth: this.getSideMenuWidth(),
sensorInfo: null,
}; };
this.lastSyncStarted_ = defaultState.syncStarted; this.lastSyncStarted_ = defaultState.syncStarted;
@ -760,6 +763,8 @@ class AppComponent extends React.Component {
await initialize(this.props.dispatch); await initialize(this.props.dispatch);
this.setState({ sensorInfo: await sensorInfo() });
this.props.dispatch({ this.props.dispatch({
type: 'APP_STATE_SET', type: 'APP_STATE_SET',
state: 'ready', state: 'ready',
@ -931,6 +936,10 @@ class AppComponent extends React.Component {
</View> </View>
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} /> <DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/> <Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
<BiometricPopup
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}
/>
</SafeAreaView> </SafeAreaView>
</MenuContext> </MenuContext>
</SideMenu> </SideMenu>

View File

@ -1611,6 +1611,28 @@ class Setting extends BaseModel {
storage: SettingStorage.Database, storage: SettingStorage.Database,
}, },
'security.biometricsEnabled': {
value: false,
type: SettingItemType.Bool,
label: () => _('Use biometrics to secure access to the app'),
public: true,
appTypes: [AppType.Mobile],
},
'security.biometricsSupportedSensors': {
value: '',
type: SettingItemType.String,
public: false,
appTypes: [AppType.Mobile],
},
'security.biometricsInitialPromptDone': {
value: false,
type: SettingItemType.Bool,
public: false,
appTypes: [AppType.Mobile],
},
// 'featureFlag.syncAccurateTimestamps': { // 'featureFlag.syncAccurateTimestamps': {
// value: false, // value: false,
// type: SettingItemType.Bool, // type: SettingItemType.Bool,

View File

@ -4725,6 +4725,7 @@ __metadata:
react-native-document-picker: 8.1.3 react-native-document-picker: 8.1.3
react-native-dropdownalert: 4.5.1 react-native-dropdownalert: 4.5.1
react-native-file-viewer: 2.1.5 react-native-file-viewer: 2.1.5
react-native-fingerprint-scanner: ^6.0.0
react-native-fs: 2.20.0 react-native-fs: 2.20.0
react-native-get-random-values: 1.8.0 react-native-get-random-values: 1.8.0
react-native-image-picker: 4.10.3 react-native-image-picker: 4.10.3
@ -27611,6 +27612,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-native-fingerprint-scanner@npm:^6.0.0":
version: 6.0.0
resolution: "react-native-fingerprint-scanner@npm:6.0.0"
peerDependencies:
react-native: ">=0.60 <1.0.0"
checksum: 67e1dcbf20d1a6119db4667162ff87c6ba606132c0cda790ef5c4d315e403f3253f8f4827373313562a261fca2d7cb77c8598f5f32d805ac07956b5301bce238
languageName: node
linkType: hard
"react-native-fs@npm:2.20.0": "react-native-fs@npm:2.20.0":
version: 2.20.0 version: 2.20.0
resolution: "react-native-fs@npm:2.20.0" resolution: "react-native-fs@npm:2.20.0"