Merge branch 'development' into nt/pin-encryption

# Conflicts:
#	src/redux/actions/applicationActions.ts
#	src/redux/constants/constants.js
#	src/redux/reducers/applicationReducer.ts
#	src/screens/pinCode/container/pinCodeContainer.tsx
#	src/screens/settings/container/settingsContainer.tsx
This commit is contained in:
Nouman Tahir 2022-07-28 12:24:53 +05:00
commit 30884383c2
15 changed files with 137 additions and 35 deletions

View File

@ -10,6 +10,8 @@
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.front" android:required="false" /> <uses-feature android:name="android.hardware.camera.front" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<queries> <queries>
<intent> <intent>

View File

@ -64,6 +64,7 @@ allprojects {
includeGroup("com.henninghall.android") includeGroup("com.henninghall.android")
includeGroup("org.matomo.sdk") includeGroup("org.matomo.sdk")
includeModule("com.yqritc", "android-scalablevideoview") includeModule("com.yqritc", "android-scalablevideoview")
includeModule("com.wei.android.lib", "fingerprintidentify")
} }
} }
} }

View File

@ -74,6 +74,8 @@
<string>Photo Library Access for allowing user to download and upload photos</string> <string>Photo Library Access for allowing user to download and upload photos</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Photo Usage Access for allowing user to upload photos</string> <string>Photo Usage Access for allowing user to upload photos</string>
<key>NSFaceIDUsageDescription</key>
<string>Ecency requires FaceID access to allow you quick and secure access.</string>
<key>UIAppFonts</key> <key>UIAppFonts</key>
<array> <array>
<string>Entypo.ttf</string> <string>Entypo.ttf</string>

View File

@ -346,6 +346,8 @@ PODS:
- React-Core - React-Core
- react-native-date-picker (4.2.0): - react-native-date-picker (4.2.0):
- React-Core - React-Core
- react-native-fingerprint-scanner (6.0.0):
- React
- react-native-matomo-sdk (0.4.1): - react-native-matomo-sdk (0.4.1):
- MatomoTracker (~> 7) - MatomoTracker (~> 7)
- React (~> 0.60) - React (~> 0.60)
@ -530,6 +532,7 @@ DEPENDENCIES:
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)" - "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
- react-native-config (from `../node_modules/react-native-config`) - react-native-config (from `../node_modules/react-native-config`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-date-picker (from `../node_modules/react-native-date-picker`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
- react-native-matomo-sdk (from `../node_modules/react-native-matomo-sdk`) - react-native-matomo-sdk (from `../node_modules/react-native-matomo-sdk`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`) - react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
@ -652,6 +655,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-config" :path: "../node_modules/react-native-config"
react-native-date-picker: react-native-date-picker:
:path: "../node_modules/react-native-date-picker" :path: "../node_modules/react-native-date-picker"
react-native-fingerprint-scanner:
:path: "../node_modules/react-native-fingerprint-scanner"
react-native-matomo-sdk: react-native-matomo-sdk:
:path: "../node_modules/react-native-matomo-sdk" :path: "../node_modules/react-native-matomo-sdk"
react-native-netinfo: react-native-netinfo:
@ -784,6 +789,7 @@ SPEC CHECKSUMS:
react-native-cameraroll: e2917a5e62da9f10c3d525e157e25e694d2d6dfa react-native-cameraroll: e2917a5e62da9f10c3d525e157e25e694d2d6dfa
react-native-config: c98128a72bc2c3a1ca72caec0b021f0fa944aa29 react-native-config: c98128a72bc2c3a1ca72caec0b021f0fa944aa29
react-native-date-picker: d83ab9cccbc497642a93fdca783ae76ecd6b17b6 react-native-date-picker: d83ab9cccbc497642a93fdca783ae76ecd6b17b6
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
react-native-matomo-sdk: 025c54f92e1e26a4d0acee7c3f28cb0fc7e4729c react-native-matomo-sdk: 025c54f92e1e26a4d0acee7c3f28cb0fc7e4729c
react-native-netinfo: 30fb89fa913c342be82a887b56e96be6d71201dd react-native-netinfo: 30fb89fa913c342be82a887b56e96be6d71201dd
react-native-orientation-locker: 2da91e5391971dace445495821c899c111dcad7a react-native-orientation-locker: 2da91e5391971dace445495821c899c111dcad7a
@ -833,4 +839,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 0282022703ad578ab2d9afbf3147ba3b373b4311 PODFILE CHECKSUM: 0282022703ad578ab2d9afbf3147ba3b373b4311
COCOAPODS: 1.11.2 COCOAPODS: 1.11.3

View File

@ -93,6 +93,7 @@
"react-native-dynamic": "^1.0.0", "react-native-dynamic": "^1.0.0",
"react-native-extended-stylesheet": "^0.10.0", "react-native-extended-stylesheet": "^0.10.0",
"react-native-fast-image": "^8.3.2", "react-native-fast-image": "^8.3.2",
"react-native-fingerprint-scanner": "hieuvp/react-native-fingerprint-scanner",
"react-native-gesture-handler": "^1.9.0", "react-native-gesture-handler": "^1.9.0",
"react-native-highlight-words": "^1.0.1", "react-native-highlight-words": "^1.0.1",
"react-native-iap": "^7.5.6", "react-native-iap": "^7.5.6",

View File

@ -17,39 +17,42 @@ const api = axios.create({
api.interceptors.request.use((request) => { api.interceptors.request.use((request) => {
console.log('Starting ecency Request', request); console.log('Starting ecency Request', request);
//skip code addition is register and token refresh endpoint is triggered //skip code addition is register and token refresh endpoint is triggered
if(request.url === '/private-api/account-create' if (request.url === '/private-api/account-create'
|| request.url === '/auth-api/hs-token-refresh' || request.url === '/auth-api/hs-token-refresh'
|| request.url === '/private-api/promoted-entries' || request.url === '/private-api/promoted-entries'
|| request.url.startsWith('private-api/leaderboard') || request.url.startsWith('private-api/leaderboard')
|| request.url.startsWith('/private-api/received-vesting/') || request.url.startsWith('/private-api/received-vesting/')
|| request.url.startsWith('/private-api/referrals/') || request.url.startsWith('/private-api/referrals/')
|| request.url.startsWith('/private-api/market-data') || request.url.startsWith('/private-api/market-data')
|| request.url.startsWith('/private-api/comment-history') || request.url.startsWith('/private-api/comment-history')
){ ) {
return request return request
} }
//decrypt access token if (!request.data?.code) {
const state = store.getState(); //if access code not already set, decrypt access token
const token = get(state, 'account.currentAccount.local.accessToken'); const state = store.getState();
const pin = get(state, 'application.pin'); const token = get(state, 'account.currentAccount.local.accessToken');
const digitPinCode = getDigitPinCode(pin); const pin = get(state, 'application.pin');
const accessToken = decryptKey(token, digitPinCode); const digitPinCode = getDigitPinCode(pin);
const accessToken = decryptKey(token, digitPinCode);
if (accessToken && !request.data?.code ) { if (accessToken) {
if (!request.data){ if (!request.data) {
request.data = {}; request.data = {};
}
request.data.code = accessToken;
console.log('Added access token:', accessToken);
} else {
const isLoggedIn = state.application.isLoggedIn;
console.warn("Failed to inject accessToken", `isLoggedIn:${isLoggedIn}`)
bugsnagInstance.notify(new Error(`Failed to inject accessToken in ${request.url} call. isLoggedIn:${isLoggedIn}, local.acccessToken:${token}, pin:${pin}`))
} }
request.data.code = accessToken;
console.log('Added access token:', accessToken);
} else {
const isLoggedIn = state.application.isLoggedIn;
console.warn("Failed to inject accessToken", `isLoggedIn:${isLoggedIn}`)
bugsnagInstance.notify(new Error(`Failed to inject accessToken in ${request.url} call. isLoggedIn:${isLoggedIn}`))
} }
return request; return request;
}); });

View File

@ -229,6 +229,7 @@
"delegations": "Delegations" "delegations": "Delegations"
}, },
"pincode": "PIN code", "pincode": "PIN code",
"biometric": "Finger Print / Face Unlock",
"reset_pin": "Reset Pin Code", "reset_pin": "Reset Pin Code",
"reset": "Reset", "reset": "Reset",
"nsfw_content": "NSFW", "nsfw_content": "NSFW",
@ -419,7 +420,8 @@
"forgot_text": "Oh, I forgot it...", "forgot_text": "Oh, I forgot it...",
"pin_not_matched":"PIN do not match, Please try again.", "pin_not_matched":"PIN do not match, Please try again.",
"attempts_postfix":"failed attempt(s)", "attempts_postfix":"failed attempt(s)",
"message_reset_warning":"User data will be wiped on next failed attempt" "message_reset_warning":"User data will be wiped on next failed attempt",
"biometric_desc":"Scan your fingerprint on the device scanner to continue"
}, },
"alert": { "alert": {
"success": "Success!", "success": "Success!",

View File

@ -32,6 +32,7 @@ import {
SET_SETTINGS_MIGRATED, SET_SETTINGS_MIGRATED,
HIDE_POSTS_THUMBNAILS, HIDE_POSTS_THUMBNAILS,
SET_TERMS_ACCEPTED, SET_TERMS_ACCEPTED,
SET_IS_BIOMETRIC_ENABLED,
SET_ENC_UNLOCK_PIN SET_ENC_UNLOCK_PIN
} from '../constants/constants'; } from '../constants/constants';
@ -210,6 +211,11 @@ export const setIsTermsAccepted = (isTermsAccepted:boolean) => ({
type: SET_TERMS_ACCEPTED type: SET_TERMS_ACCEPTED
}) })
export const setIsBiometricEnabled = (enabled:boolean) => ({
payload:enabled,
type: SET_IS_BIOMETRIC_ENABLED
})
export const setEncryptedUnlockPin = (encryptedUnlockPin:string) => ({ export const setEncryptedUnlockPin = (encryptedUnlockPin:string) => ({
payload:encryptedUnlockPin, payload:encryptedUnlockPin,
type: SET_ENC_UNLOCK_PIN type: SET_ENC_UNLOCK_PIN

View File

@ -36,6 +36,7 @@ export const SET_LAST_APP_VERSION = 'SET_LAST_APP_VERSION';
export const SET_COLOR_THEME = 'SET_COLOR_THEME'; export const SET_COLOR_THEME = 'SET_COLOR_THEME';
export const SET_SETTINGS_MIGRATED = 'SET_SETTINGS_MIGRATED'; export const SET_SETTINGS_MIGRATED = 'SET_SETTINGS_MIGRATED';
export const SET_TERMS_ACCEPTED = 'SET_TERMS_ACCEPTED'; export const SET_TERMS_ACCEPTED = 'SET_TERMS_ACCEPTED';
export const SET_IS_BIOMETRIC_ENABLED = 'SET_IS_BIOMETRIC_ENABLED';
export const SET_ENC_UNLOCK_PIN = 'SET_ENC_UNLOCK_PIN'; export const SET_ENC_UNLOCK_PIN = 'SET_ENC_UNLOCK_PIN';
// Accounts // Accounts

View File

@ -30,6 +30,7 @@ import {
SET_SETTINGS_MIGRATED, SET_SETTINGS_MIGRATED,
HIDE_POSTS_THUMBNAILS, HIDE_POSTS_THUMBNAILS,
SET_TERMS_ACCEPTED, SET_TERMS_ACCEPTED,
SET_IS_BIOMETRIC_ENABLED,
SET_ENC_UNLOCK_PIN SET_ENC_UNLOCK_PIN
} from '../constants/constants'; } from '../constants/constants';
@ -71,6 +72,7 @@ interface State {
settingsMigratedV2: boolean; settingsMigratedV2: boolean;
hidePostsThumbnails: boolean; hidePostsThumbnails: boolean;
isTermsAccepted: boolean; isTermsAccepted: boolean;
isBiometricEnabled: boolean;
} }
const initialState:State = { const initialState:State = {
@ -111,6 +113,7 @@ const initialState:State = {
settingsMigratedV2: false, settingsMigratedV2: false,
hidePostsThumbnails: false, hidePostsThumbnails: false,
isTermsAccepted: false, isTermsAccepted: false,
isBiometricEnabled: false
}; };
export default function (state = initialState, action):State { export default function (state = initialState, action):State {
@ -285,6 +288,12 @@ export default function (state = initialState, action):State {
...state, ...state,
isTermsAccepted:action.payload isTermsAccepted:action.payload
} }
case SET_IS_BIOMETRIC_ENABLED:
return {
...state,
isBiometricEnabled:action.payload
}
case SET_ENC_UNLOCK_PIN: case SET_ENC_UNLOCK_PIN:
return { return {

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import Config from 'react-native-config'; import Config from 'react-native-config';
import get from 'lodash/get'; import get from 'lodash/get';
import FingerprintScanner from 'react-native-fingerprint-scanner';
// Actions & Services // Actions & Services
@ -37,6 +38,8 @@ import PinCodeScreen from '../screen/pinCodeScreen';
class PinCodeContainer extends Component { class PinCodeContainer extends Component {
screenRef = null;
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -74,9 +77,43 @@ class PinCodeContainer extends Component {
} }
_processBiometric = async () => {
try {
const {
intl,
pinCodeParams: { isReset },
applicationPinCode,
isBiometricEnabled,
} = this.props;
if (isReset || !isBiometricEnabled) {
return;
}
const biometryType = await FingerprintScanner.isSensorAvailable();
console.log('biometryType is => ', biometryType);
await FingerprintScanner.authenticate({
description: intl.formatMessage({ id: 'pincode.biometric_desc' }),
});
console.log('successfully passed biometric auth');
//code gets here means biometeric succeeded
if (this.screenRef) {
const verifiedPin = decryptKey(applicationPinCode, Config.PIN_KEY, this._onDecryptFail);
this.screenRef.setPinThroughBiometric(verifiedPin);
}
} catch (err) {
console.warn('Failed to process biometric', err);
}
FingerprintScanner.release();
};
//this function updates realm with appropriate master key required for encyrption //this function updates realm with appropriate master key required for encyrption
//this function is important: must run while chaning pin //this function is important: must run while chaning pin
//and even logging in with existing pin code //and even logging in with existing pin code
_updatePinCodeRealm = async (pinData) => { _updatePinCodeRealm = async (pinData) => {
try { try {
const { currentAccount, dispatch } = this.props; const { currentAccount, dispatch } = this.props;
@ -388,12 +425,14 @@ class PinCodeContainer extends Component {
return ( return (
<PinCodeScreen <PinCodeScreen
ref={(ref) => (this.screenRef = ref)}
informationText={informationText} informationText={informationText}
setPinCode={(pin) => this._setPinCode(pin, isReset)} setPinCode={(pin) => this._setPinCode(pin, isReset)}
showForgotButton={!isOldPinVerified} showForgotButton={!isOldPinVerified}
username={currentAccount.name} username={currentAccount.name}
intl={intl} intl={intl}
handleForgotButton={() => this._handleForgotButton()} handleForgotButton={() => this._handleForgotButton()}
isReset={isReset}
{...this.props} {...this.props}
/> />
); );
@ -406,6 +445,7 @@ const mapStateToProps = (state) => ({
encUnlockPin: state.application.encUnlockPin, encUnlockPin: state.application.encUnlockPin,
otherAccounts: state.account.otherAccounts, otherAccounts: state.account.otherAccounts,
pinCodeParams: state.application.pinCodeNavigation, pinCodeParams: state.application.pinCodeNavigation,
isBiometricEnabled: state.application.isBiometricEnabled,
}); });
export default injectIntl(connect(mapStateToProps)(PinCodeContainer)); export default injectIntl(connect(mapStateToProps)(PinCodeContainer));

View File

@ -1,27 +1,39 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Text, TouchableOpacity, View } from 'react-native'; import { Text, TouchableOpacity, View } from 'react-native';
import { NumericKeyboard, PinAnimatedInput, UserAvatar } from '../../../components'; import { NumericKeyboard, PinAnimatedInput, UserAvatar } from '../../../components';
import styles from './pinCodeStyles'; import styles from './pinCodeStyles';
const PinCodeScreen = ({ const PinCodeScreen = forwardRef(({
informationText, informationText,
showForgotButton, showForgotButton,
username, username,
handleForgotButton, handleForgotButton,
setPinCode, setPinCode,
}) => { }, ref) => {
const [pin, setPin] = useState(''); const [pin, setPin] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const intl = useIntl(); const intl = useIntl();
useImperativeHandle(ref, () => ({
setPinThroughBiometric(bioPin){
if(bioPin && bioPin.length === 4){
setLoading(true);
setPin(bioPin)
}
}
}));
useEffect(() => { useEffect(() => {
_handlePinComplete(); _handlePinComplete();
}, [pin]); }, [pin]);
const _handlePinComplete = async () => { const _handlePinComplete = async () => {
if (pin.length === 4) { if (pin.length === 4) {
setLoading(true); setLoading(true);
@ -31,6 +43,7 @@ const PinCodeScreen = ({
} }
}; };
const _handleKeyboardOnPress = async (value) => { const _handleKeyboardOnPress = async (value) => {
try { try {
if (loading) { if (loading) {
@ -82,6 +95,6 @@ const PinCodeScreen = ({
)} )}
</View> </View>
); );
}; })
export default PinCodeScreen; export default PinCodeScreen;

View File

@ -43,6 +43,7 @@ import {
logoutDone, logoutDone,
closePinCodeModal, closePinCodeModal,
setColorTheme, setColorTheme,
setIsBiometricEnabled,
setEncryptedUnlockPin, setEncryptedUnlockPin,
} from '../../../redux/actions/applicationActions'; } from '../../../redux/actions/applicationActions';
import { toastNotification } from '../../../redux/actions/uiAction'; import { toastNotification } from '../../../redux/actions/uiAction';
@ -240,6 +241,14 @@ class SettingsContainer extends Component {
); );
} }
break; break;
case 'biometric':
dispatch(
openPinCodeModal({
callback: () => dispatch(setIsBiometricEnabled(action)),
}),
);
break;
default: default:
break; break;
} }
@ -460,6 +469,7 @@ const mapStateToProps = (state) => ({
colorTheme: state.application.colorTheme, colorTheme: state.application.colorTheme,
isPinCodeOpen: state.application.isPinCodeOpen, isPinCodeOpen: state.application.isPinCodeOpen,
encUnlockPin: state.application.encUnlockPin, encUnlockPin: state.application.encUnlockPin,
isBiometricEnabled: state.application.isBiometricEnabled,
isDefaultFooter: state.application.isDefaultFooter, isDefaultFooter: state.application.isDefaultFooter,
isLoggedIn: state.application.isLoggedIn, isLoggedIn: state.application.isLoggedIn,
isNotificationSettingsOpen: state.application.isNotificationOpen, isNotificationSettingsOpen: state.application.isNotificationOpen,

View File

@ -24,6 +24,7 @@ const SettingsScreen = ({
isDarkTheme, isDarkTheme,
colorThemeIndex, colorThemeIndex,
isPinCodeOpen, isPinCodeOpen,
isBiometricEnabled,
isLoggedIn, isLoggedIn,
isNotificationSettingsOpen, isNotificationSettingsOpen,
nsfw, nsfw,
@ -147,6 +148,16 @@ const SettingsScreen = ({
{!!isLoggedIn && !!isPinCodeOpen && ( {!!isLoggedIn && !!isPinCodeOpen && (
<Fragment> <Fragment>
<SettingsItem
title={intl.formatMessage({
id: 'settings.biometric',
})}
type="toggle"
actionType="biometric"
isOn={isBiometricEnabled}
handleOnChange={handleOnChange}
/>
<SettingsItem <SettingsItem
title={intl.formatMessage({ title={intl.formatMessage({
id: 'settings.reset_pin', id: 'settings.reset_pin',
@ -159,15 +170,6 @@ const SettingsScreen = ({
handleOnButtonPress={handleOnButtonPress} handleOnButtonPress={handleOnButtonPress}
/> />
</Fragment> </Fragment>
// <SettingsItem
// title={intl.formatMessage({
// id: 'settings.default_footer',
// })}
// type="toggle"
// actionType="default_footer"
// isOn={isDefaultFooter}
// handleOnChange={handleOnChange}
// />
)} )}
</View> </View>
{!!isLoggedIn && ( {!!isLoggedIn && (

View File

@ -8782,6 +8782,10 @@ react-native-fast-image@^8.3.2:
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.3.4.tgz#79edca177e30311b19d59ff335625bcbe22650d7" resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.3.4.tgz#79edca177e30311b19d59ff335625bcbe22650d7"
integrity sha512-LpzAdjUphihUpVEBn5fEv5AILe55rHav0YiZroPZ1rumKDhAl4u2cG01ku2Pb7l8sayjTsNu7FuURAlXUUDsow== integrity sha512-LpzAdjUphihUpVEBn5fEv5AILe55rHav0YiZroPZ1rumKDhAl4u2cG01ku2Pb7l8sayjTsNu7FuURAlXUUDsow==
react-native-fingerprint-scanner@hieuvp/react-native-fingerprint-scanner:
version "6.0.0"
resolved "https://codeload.github.com/hieuvp/react-native-fingerprint-scanner/tar.gz/9cecc0db326471c571553ea85f7c016fee2f803d"
react-native-flipper@^0.34.0: react-native-flipper@^0.34.0:
version "0.34.0" version "0.34.0"
resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d" resolved "https://registry.yarnpkg.com/react-native-flipper/-/react-native-flipper-0.34.0.tgz#7df1f38ba5d97a9321125fe0fccbe47d99e6fa1d"