fix: move wrapped storage to unwrapped if not encrypted (#1603)

This commit is contained in:
Mo
2022-09-20 12:18:00 -05:00
committed by GitHub
parent cb4c0f1b5c
commit c5e225d335
21 changed files with 134 additions and 56 deletions

View File

@@ -1,2 +1,19 @@
node_modules node_modules
scripts scripts
actions
.yarn
.vscode
.sass-cache
.husky
.github
.git
**/dist
**/coverage
**/node_modules
packages/components
packages/docs
packages/mobile/android
packages/mobile/ios
packages/mobile/html
packages/snjs/mocha
*.d.ts

View File

@@ -1,4 +1,4 @@
{ {
"extends": ["./node_modules/@standardnotes/config/src/.eslintrc"], "extends": ["./node_modules/@standardnotes/config/src/.eslintrc"],
"ignorePatterns": [".eslintrc.js", "*.webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__"] "ignorePatterns": [".eslintrc.js", "*.webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__", "**/**/coverage"]
} }

View File

@@ -1,2 +1,3 @@
node_modules node_modules
dist dist
coverage

View File

@@ -0,0 +1,3 @@
node_modules
dist
jsign

View File

@@ -5,4 +5,5 @@ ios
e2e e2e
android android
fastlane fastlane
WebFrame WebFrame
__tests__

View File

@@ -570,7 +570,7 @@ export class ApplicationState extends ApplicationService {
).includes(state) ).includes(state)
} }
private async getScreenshotPrivacyEnabled(): Promise<boolean | undefined> { private async getScreenshotPrivacyEnabled(): Promise<boolean> {
return this.application.getMobileScreenshotPrivacyEnabled() return this.application.getMobileScreenshotPrivacyEnabled()
} }

View File

@@ -73,6 +73,11 @@ export class MobileDevice implements MobileDeviceInterface {
;(this.stateObserverService as unknown) = undefined ;(this.stateObserverService as unknown) = undefined
} }
consoleLog(...args: any[]): void {
// eslint-disable-next-line no-console
console.log(args)
}
async setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void> { async setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void> {
await Keychain.setKeys(value) await Keychain.setKeys(value)
} }
@@ -378,7 +383,7 @@ export class MobileDevice implements MobileDeviceInterface {
await Keychain.clearKeys() await Keychain.clearKeys()
} }
async setAndroidScreenshotPrivacy(enable: boolean): Promise<void> { setAndroidScreenshotPrivacy(enable: boolean): void {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
enable ? FlagSecure.activate() : FlagSecure.deactivate() enable ? FlagSecure.activate() : FlagSecure.deactivate()
} }

View File

@@ -38,14 +38,14 @@ export const SecuritySection = (props: Props) => {
useEffect(() => { useEffect(() => {
let mounted = true let mounted = true
const getHasScreenshotPrivacy = async () => { const getHasScreenshotPrivacy = async () => {
const hasScreenshotPrivacyEnabled = (await application?.getMobileScreenshotPrivacyEnabled()) ?? true const hasScreenshotPrivacyEnabled = application?.getMobileScreenshotPrivacyEnabled()
if (mounted) { if (mounted) {
setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled) setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled)
} }
} }
void getHasScreenshotPrivacy() void getHasScreenshotPrivacy()
const getHasBiometrics = async () => { const getHasBiometrics = async () => {
const appHasBiometrics = await application!.hasBiometrics() const appHasBiometrics = application!.hasBiometrics()
if (mounted) { if (mounted) {
setHasBiometrics(appHasBiometrics) setHasBiometrics(appHasBiometrics)
} }

View File

@@ -1,2 +1,3 @@
node_modules node_modules
dist dist
coverage

View File

@@ -6,8 +6,9 @@ export interface MobileDeviceInterface extends DeviceInterface {
getRawKeychainValue(): Promise<RawKeychainValue | undefined> getRawKeychainValue(): Promise<RawKeychainValue | undefined>
getDeviceBiometricsAvailability(): Promise<boolean> getDeviceBiometricsAvailability(): Promise<boolean>
setAndroidScreenshotPrivacy(enable: boolean): Promise<void> setAndroidScreenshotPrivacy(enable: boolean): void
authenticateWithBiometrics(): Promise<boolean> authenticateWithBiometrics(): Promise<boolean>
hideMobileInterfaceFromScreenshots(): void hideMobileInterfaceFromScreenshots(): void
stopHidingMobileInterfaceFromScreenshots(): void stopHidingMobileInterfaceFromScreenshots(): void
consoleLog(...args: any[]): void
} }

View File

@@ -2,4 +2,8 @@ node_modules
dist dist
test test
*.config.js *.config.js
mocha/**/* mocha/**/*
coverage
e2e-server.js
jest-global.ts
webpack.*.js

View File

@@ -324,6 +324,7 @@ export class SNApplication
) )
} }
} }
await this.handleStage(ExternalServices.ApplicationStage.StorageDecrypted_09) await this.handleStage(ExternalServices.ApplicationStage.StorageDecrypted_09)
this.apiService.loadHost() this.apiService.loadHost()
@@ -929,30 +930,30 @@ export class SNApplication
return this.deinit(this.getDeinitMode(), DeinitSource.Lock) return this.deinit(this.getDeinitMode(), DeinitSource.Lock)
} }
async setBiometricsTiming(timing: MobileUnlockTiming) { setBiometricsTiming(timing: MobileUnlockTiming) {
return this.protectionService.setBiometricsTiming(timing) return this.protectionService.setBiometricsTiming(timing)
} }
async getMobileScreenshotPrivacyEnabled(): Promise<boolean | undefined> { getMobileScreenshotPrivacyEnabled(): boolean {
return this.protectionService.getMobileScreenshotPrivacyEnabled() return this.protectionService.getMobileScreenshotPrivacyEnabled()
} }
async getMobilePasscodeTiming(): Promise<MobileUnlockTiming | undefined> { setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
return this.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise<
MobileUnlockTiming | undefined
>
}
async getMobileBiometricsTiming(): Promise<MobileUnlockTiming | undefined> {
return this.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as Promise<
MobileUnlockTiming | undefined
>
}
async setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
return this.protectionService.setMobileScreenshotPrivacyEnabled(isEnabled) return this.protectionService.setMobileScreenshotPrivacyEnabled(isEnabled)
} }
getMobilePasscodeTiming(): MobileUnlockTiming | undefined {
return this.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as
| MobileUnlockTiming
| undefined
}
getMobileBiometricsTiming(): MobileUnlockTiming | undefined {
return this.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as
| MobileUnlockTiming
| undefined
}
async loadMobileUnlockTiming() { async loadMobileUnlockTiming() {
return this.protectionService.loadMobileUnlockTiming() return this.protectionService.loadMobileUnlockTiming()
} }

View File

@@ -257,36 +257,36 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
] ]
} }
private async getBiometricsTiming(): Promise<MobileUnlockTiming | undefined> { private getBiometricsTiming(): MobileUnlockTiming | undefined {
return this.storageService.getValue<Promise<MobileUnlockTiming | undefined>>( return this.storageService.getValue<MobileUnlockTiming | undefined>(
StorageKey.MobileBiometricsTiming, StorageKey.MobileBiometricsTiming,
StorageValueModes.Nonwrapped, StorageValueModes.Nonwrapped,
) )
} }
private async getPasscodeTiming(): Promise<MobileUnlockTiming | undefined> { private getPasscodeTiming(): MobileUnlockTiming | undefined {
return this.storageService.getValue<Promise<MobileUnlockTiming | undefined>>( return this.storageService.getValue<MobileUnlockTiming | undefined>(
StorageKey.MobilePasscodeTiming, StorageKey.MobilePasscodeTiming,
StorageValueModes.Nonwrapped, StorageValueModes.Nonwrapped,
) )
} }
async setBiometricsTiming(timing: MobileUnlockTiming) { async setBiometricsTiming(timing: MobileUnlockTiming) {
await this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
this.mobileBiometricsTiming = timing this.mobileBiometricsTiming = timing
} }
async setMobileScreenshotPrivacyEnabled(isEnabled: boolean) { setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default) return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default)
} }
async getMobileScreenshotPrivacyEnabled(): Promise<boolean | undefined> { getMobileScreenshotPrivacyEnabled(): boolean {
return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default) return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false)
} }
async loadMobileUnlockTiming() { loadMobileUnlockTiming(): void {
this.mobilePasscodeTiming = await this.getPasscodeTiming() this.mobilePasscodeTiming = this.getPasscodeTiming()
this.mobileBiometricsTiming = await this.getBiometricsTiming() this.mobileBiometricsTiming = this.getBiometricsTiming()
} }
private async validateOrRenewSession( private async validateOrRenewSession(

View File

@@ -0,0 +1,33 @@
import { DiskStorageService } from './DiskStorageService'
import { InternalEventBus, DeviceInterface, InternalEventBusInterface } from '@standardnotes/services'
import { Environment } from '@standardnotes/models'
describe('diskStorageService', () => {
let storageService: DiskStorageService
let internalEventBus: InternalEventBusInterface
let device: DeviceInterface
beforeEach(() => {
internalEventBus = {} as jest.Mocked<InternalEventBus>
device = {} as jest.Mocked<DeviceInterface>
storageService = new DiskStorageService(device, 'test', Environment.Desktop, internalEventBus)
})
it('setInitialValues should set unwrapped values as wrapped value if wrapped value is not encrypted', async () => {
storageService.isStorageWrapped = jest.fn().mockReturnValue(false)
await storageService['setInitialValues']({
wrapped: { content: { foo: 'bar' } } as never,
nonwrapped: {},
unwrapped: { bar: 'zoo' },
})
expect(storageService['values']).toEqual({
wrapped: { content: { foo: 'bar' } } as never,
nonwrapped: {},
unwrapped: { bar: 'zoo', foo: 'bar' },
})
})
})

View File

@@ -112,14 +112,10 @@ export class DiskStorageService extends Services.AbstractService implements Serv
const value = await this.deviceInterface.getRawStorageValue(this.getPersistenceKey()) const value = await this.deviceInterface.getRawStorageValue(this.getPersistenceKey())
const values = value ? JSON.parse(value as string) : undefined const values = value ? JSON.parse(value as string) : undefined
this.setInitialValues(values) await this.setInitialValues(values)
} }
/** private async setInitialValues(values?: Services.StorageValuesObject) {
* Called by platforms with the value they load from disk,
* after they handle initializeFromDisk
*/
private setInitialValues(values?: Services.StorageValuesObject) {
const sureValues = values || this.defaultValuesObject() const sureValues = values || this.defaultValuesObject()
if (!sureValues[Services.ValueModesKeys.Unwrapped]) { if (!sureValues[Services.ValueModesKeys.Unwrapped]) {
@@ -127,6 +123,13 @@ export class DiskStorageService extends Services.AbstractService implements Serv
} }
this.values = sureValues this.values = sureValues
if (!this.isStorageWrapped()) {
this.values[Services.ValueModesKeys.Unwrapped] = {
...(this.values[Services.ValueModesKeys.Wrapped].content as object),
...this.values[Services.ValueModesKeys.Unwrapped],
}
}
} }
public isStorageWrapped(): boolean { public isStorageWrapped(): boolean {
@@ -370,7 +373,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv
* Clears simple values from storage only. Does not affect payloads. * Clears simple values from storage only. Does not affect payloads.
*/ */
async clearValues() { async clearValues() {
this.setInitialValues() await this.setInitialValues()
await this.immediatelyPersistValuesToDisk() await this.immediatelyPersistValuesToDisk()
} }

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
extends: ['../.eslintrc.js'], extends: ['../.eslintrc'],
globals: { globals: {
chai: true, chai: true,
chaiAsPromised: true, chaiAsPromised: true,

View File

@@ -0,0 +1,4 @@
dist
node_modules
web.webpack-defaults.js
coverage

View File

@@ -19,7 +19,9 @@
"__mocks__", "__mocks__",
"src/components", "src/components",
"src/favicon", "src/favicon",
"src/vendor" "src/vendor",
"coverage",
"*.config.js"
], ],
"rules": { "rules": {
"standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals "standard/no-callback-literal": 0, // Disable this as we have too many callbacks relying on literals

View File

@@ -76,6 +76,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
if (this.isNativeMobileWeb()) { if (this.isNativeMobileWeb()) {
this.mobileWebReceiver = new MobileWebReceiver(this) this.mobileWebReceiver = new MobileWebReceiver(this)
// eslint-disable-next-line no-console
console.log = (...args) => {
this.mobileDevice.consoleLog(...args)
}
} }
this.onVisibilityChange = () => { this.onVisibilityChange = () => {
@@ -228,7 +233,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
async handleMobileGainingFocusEvent(): Promise<void> {} async handleMobileGainingFocusEvent(): Promise<void> {}
async handleMobileLosingFocusEvent(): Promise<void> { async handleMobileLosingFocusEvent(): Promise<void> {
if (await this.getMobileScreenshotPrivacyEnabled()) { if (this.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots() this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
} }
@@ -236,7 +241,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
} }
async handleMobileResumingFromBackgroundEvent(): Promise<void> { async handleMobileResumingFromBackgroundEvent(): Promise<void> {
if (await this.getMobileScreenshotPrivacyEnabled()) { if (this.getMobileScreenshotPrivacyEnabled()) {
this.mobileDevice.hideMobileInterfaceFromScreenshots() this.mobileDevice.hideMobileInterfaceFromScreenshots()
} }
} }

View File

@@ -20,7 +20,7 @@ const BiometricsLock = ({ application }: Props) => {
useEffect(() => { useEffect(() => {
const getHasBiometrics = async () => { const getHasBiometrics = async () => {
const appHasBiometrics = await application.hasBiometrics() const appHasBiometrics = application.hasBiometrics()
setHasBiometrics(appHasBiometrics) setHasBiometrics(appHasBiometrics)
} }
@@ -50,7 +50,7 @@ const BiometricsLock = ({ application }: Props) => {
await disableBiometrics() await disableBiometrics()
} else { } else {
setHasBiometrics(true) setHasBiometrics(true)
await application.enableBiometrics() application.enableBiometrics()
await setBiometricsTimingValue(MobileUnlockTiming.OnQuit) await setBiometricsTimingValue(MobileUnlockTiming.OnQuit)
} }
} }

View File

@@ -13,22 +13,19 @@ type Props = {
} }
const MultitaskingPrivacy = ({ application }: Props) => { const MultitaskingPrivacy = ({ application }: Props) => {
const [hasScreenshotPrivacy, setHasScreenshotPrivacy] = useState<boolean | undefined>(false) const [hasScreenshotPrivacy, setHasScreenshotPrivacy] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
const getHasScreenshotPrivacy = async () => { const hasScreenshotPrivacyEnabled = application.getMobileScreenshotPrivacyEnabled()
const hasScreenshotPrivacyEnabled = (await application.getMobileScreenshotPrivacyEnabled()) ?? true setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled)
setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled)
}
void getHasScreenshotPrivacy()
}, [application]) }, [application])
const onScreenshotPrivacyPress = async () => { const onScreenshotPrivacyPress = async () => {
const enable = !hasScreenshotPrivacy const enable = !hasScreenshotPrivacy
setHasScreenshotPrivacy(enable) setHasScreenshotPrivacy(enable)
await application.setMobileScreenshotPrivacyEnabled(enable) application.setMobileScreenshotPrivacyEnabled(enable)
await (application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enable) ;(application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enable)
} }
const screenshotPrivacyFeatureText = isIOS() ? 'Multitasking Privacy' : 'Multitasking/Screenshot Privacy' const screenshotPrivacyFeatureText = isIOS() ? 'Multitasking Privacy' : 'Multitasking/Screenshot Privacy'