feat: mobile security prefs (#1496)

* feat: move mobile-specific security items to Web when rendered in WebView

* feat: better UI for biometrics section

* feat: move Multitasking Privacy section to WebView (mostly UI)

* feat: move Multitasking Privacy section to WebView (going to understand why in WebView multitasking privacy value is auto-changed after reopening the WebView)

* feat: store MultitaskingPrivacy value as "NonWrapped" so that it's the same both on mobile and WebView

* feat: open WebView correctly when "Storage Encryption" is disabled on mobile

* fix: remove unnecessary changes and comments

* chore: revert ios-related unneeded changes

* fix: let Android to correctly recognize the NativeMobileWeb environment when opening WebView on Android

* fix: correct styles for the selected state of Biometrics/Passcode options

* chore: code cleanup

* fix: store Multitasking/Screenshot Privacy in the `Default` storage value mode

* chore: remove comment

* fix: use application's method instead of directly updating Screenshot Privacy preference

* fix: remove unused variable

* fix: use methods from Application and MobileDeviceInterface in all places, remove duplicate code

* fix: hide Multitasking Privacy and Biometrics Lock in WebView

Co-authored-by: Aman Harwara
This commit is contained in:
Vardan Hakobyan
2022-09-14 13:07:10 +04:00
committed by GitHub
parent e73d187b65
commit d7aca2c13a
14 changed files with 324 additions and 92 deletions

View File

@@ -9,6 +9,7 @@ import {
Environment,
IconsController,
ItemGroupController,
MobileUnlockTiming,
platformFromString,
SNApplication,
SNComponentManager,
@@ -17,7 +18,7 @@ import { Platform } from 'react-native'
import { version } from '../../package.json'
import { MobileAlertService } from './AlertService'
import { ApplicationState, UnlockTiming } from './ApplicationState'
import { ApplicationState } from './ApplicationState'
import { BackupsService } from './BackupsService'
import { ComponentManager } from './ComponentManager'
import { FilesService } from './FilesService'
@@ -122,7 +123,7 @@ export class MobileApplication extends SNApplication {
const previouslyLaunched = MobileApplication.getPreviouslyLaunched()
const biometricsTiming = this.getAppState().biometricsTiming
if (previouslyLaunched && biometricsTiming === UnlockTiming.OnQuit) {
if (previouslyLaunched && biometricsTiming === MobileUnlockTiming.OnQuit) {
const filteredPrompts = challenge.prompts.filter(
(prompt: ChallengePrompt) => prompt.validation !== ChallengeValidation.Biometric,
)

View File

@@ -1,3 +1,4 @@
import { MobileDeviceInterface } from '@Lib/Interface'
import {
ApplicationEvent,
ApplicationService,
@@ -8,6 +9,7 @@ import {
ContentType,
InternalEventBus,
isNullOrUndefined,
MobileUnlockTiming,
NoteViewController,
PayloadEmitSource,
PrefKey,
@@ -30,14 +32,13 @@ import {
KeyboardEventListener,
NativeEventSubscription,
NativeModules,
Platform,
} from 'react-native'
import FlagSecure from 'react-native-flag-secure-android'
import { hide, show } from 'react-native-privacy-snapshot'
import VersionInfo from 'react-native-version-info'
import pjson from '../../package.json'
import { MobileApplication } from './Application'
import { associateComponentWithNote } from './ComponentManager'
const { PlatformConstants } = NativeModules
export enum AppStateType {
@@ -66,11 +67,6 @@ export type TabletModeChangeData = {
old_isInTabletMode: boolean
}
export enum UnlockTiming {
Immediately = 'immediately',
OnQuit = 'on-quit',
}
export enum PasscodeKeyboardType {
Default = 'default',
Numeric = 'numeric',
@@ -103,8 +99,8 @@ export class ApplicationState extends ApplicationService {
authenticationInProgress = false
multiEditorEnabled = false
screenshotPrivacyEnabled?: boolean
passcodeTiming?: UnlockTiming
biometricsTiming?: UnlockTiming
passcodeTiming?: MobileUnlockTiming
biometricsTiming?: MobileUnlockTiming
removeHandleReactNativeAppStateChangeListener: NativeEventSubscription
removeItemChangesListener?: () => void
removePreferencesLoadedListener?: () => void
@@ -173,7 +169,9 @@ export class ApplicationState extends ApplicationService {
override async onAppLaunch() {
MobileApplication.setPreviouslyLaunched()
this.screenshotPrivacyEnabled = (await this.getScreenshotPrivacyEnabled()) ?? true
void this.setAndroidScreenshotPrivacy(this.screenshotPrivacyEnabled)
await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(
this.screenshotPrivacyEnabled,
)
}
/**
@@ -248,12 +246,6 @@ export class ApplicationState extends ApplicationService {
this.biometricsTiming = await this.getBiometricsTiming()
}
public async setAndroidScreenshotPrivacy(enable: boolean) {
if (Platform.OS === 'android') {
enable ? FlagSecure.activate() : FlagSecure.deactivate()
}
}
/**
* Creates a new editor if one doesn't exist. If one does, we'll replace the
* editor's note with an empty one.
@@ -486,44 +478,14 @@ export class ApplicationState extends ApplicationService {
}
}
getPasscodeTimingOptions() {
return [
{
title: 'Immediately',
key: UnlockTiming.Immediately,
selected: this.passcodeTiming === UnlockTiming.Immediately,
},
{
title: 'On Quit',
key: UnlockTiming.OnQuit,
selected: this.passcodeTiming === UnlockTiming.OnQuit,
},
]
}
getBiometricsTimingOptions() {
return [
{
title: 'Immediately',
key: UnlockTiming.Immediately,
selected: this.biometricsTiming === UnlockTiming.Immediately,
},
{
title: 'On Quit',
key: UnlockTiming.OnQuit,
selected: this.biometricsTiming === UnlockTiming.OnQuit,
},
]
}
private async checkAndLockApplication() {
const isLocked = await this.application.isLocked()
if (!isLocked) {
const hasBiometrics = await this.application.hasBiometrics()
const hasPasscode = this.application.hasPasscode()
if (hasPasscode && this.passcodeTiming === UnlockTiming.Immediately) {
if (hasPasscode && this.passcodeTiming === MobileUnlockTiming.Immediately) {
await this.application.lock()
} else if (hasBiometrics && this.biometricsTiming === UnlockTiming.Immediately && !this.locked) {
} else if (hasBiometrics && this.biometricsTiming === MobileUnlockTiming.Immediately && !this.locked) {
const challenge = new Challenge(
[new ChallengePrompt(ChallengeValidation.Biometric)],
ChallengeReason.ApplicationUnlock,
@@ -604,35 +566,33 @@ export class ApplicationState extends ApplicationService {
}
private async getScreenshotPrivacyEnabled(): Promise<boolean | undefined> {
return this.application.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default) as Promise<
boolean | undefined
>
return this.application.getMobileScreenshotPrivacyEnabled()
}
private async getPasscodeTiming(): Promise<UnlockTiming | undefined> {
private async getPasscodeTiming(): Promise<MobileUnlockTiming | undefined> {
return this.application.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise<
UnlockTiming | undefined
MobileUnlockTiming | undefined
>
}
private async getBiometricsTiming(): Promise<UnlockTiming | undefined> {
private async getBiometricsTiming(): Promise<MobileUnlockTiming | undefined> {
return this.application.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as Promise<
UnlockTiming | undefined
MobileUnlockTiming | undefined
>
}
public async setScreenshotPrivacyEnabled(enabled: boolean) {
await this.application.setValue(StorageKey.MobileScreenshotPrivacyEnabled, enabled, StorageValueModes.Default)
await this.application.setMobileScreenshotPrivacyEnabled(enabled)
this.screenshotPrivacyEnabled = enabled
void this.setAndroidScreenshotPrivacy(enabled)
await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enabled)
}
public async setPasscodeTiming(timing: UnlockTiming) {
public async setPasscodeTiming(timing: MobileUnlockTiming) {
await this.application.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
this.passcodeTiming = timing
}
public async setBiometricsTiming(timing: UnlockTiming) {
public async setBiometricsTiming(timing: MobileUnlockTiming) {
await this.application.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
this.biometricsTiming = timing
}

View File

@@ -12,6 +12,7 @@ import {
} from '@standardnotes/snjs'
import { Alert, Linking, Platform } from 'react-native'
import FingerprintScanner from 'react-native-fingerprint-scanner'
import FlagSecure from 'react-native-flag-secure-android'
import Keychain from './Keychain'
export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID'
@@ -295,6 +296,12 @@ export class MobileDeviceInterface implements DeviceInterface {
await Keychain.clearKeys()
}
async setAndroidScreenshotPrivacy(enable: boolean): Promise<void> {
if (Platform.OS === 'android') {
enable ? FlagSecure.activate() : FlagSecure.deactivate()
}
}
openUrl(url: string) {
const showAlert = () => {
Alert.alert('Unable to Open', `Unable to open URL ${url}.`)