From 3c332a35f6024c46020a1d6ac68a5df989804142 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 3 Jan 2023 14:15:45 -0600 Subject: [PATCH] feat: improve initial load performance on mobile (#2126) --- ...e-mmkv-npm-2.5.1-4745a42823-6f0cf484e7.zip | Bin 0 -> 259912 bytes .../example/src/web_device_interface.js | 29 +- packages/mobile/ios/Podfile.lock | 18 +- packages/mobile/ios/StandardNotes/Info.plist | 4 +- packages/mobile/package.json | 1 + packages/mobile/src/Lib/Database/Database.ts | 158 +++ .../src/Lib/Database/DatabaseInterface.ts | 10 + .../src/Lib/Database/DatabaseMetadata.ts | 39 + .../src/Lib/Database/FlashKeyValueStore.ts | 40 + .../src/Lib/Database/LegacyIdentifier.ts | 15 + .../src/Lib/Database/LegacyKeyValueStore.ts | 26 + .../Lib/Database/showLoadFailForItemIds.ts | 16 + .../src/Lib/{Interface.ts => MobileDevice.ts} | 331 ++---- packages/mobile/src/MobileWebAppContainer.tsx | 6 +- .../src/Domain/Local/RootKey/KeychainTypes.ts | 19 - .../Application/ApplicationInterface.ts | 1 - .../src/Domain/Device/DatabaseItemMetadata.ts | 3 + .../src/Domain/Device/DatabaseLoadOptions.ts | 44 + .../Domain/Device/DatabaseLoadSorter.spec.ts} | 28 +- .../src/Domain/Device/DatabaseLoadSorter.ts} | 49 +- .../src/Domain/Device/DeviceInterface.ts | 33 +- .../Domain/Storage/StorageServiceInterface.ts | 8 +- .../services/src/Domain/Sync/SyncOptions.ts | 1 + packages/services/src/Domain/index.ts | 3 + .../snjs/lib/Application/Application.spec.ts | 6 +- packages/snjs/lib/Application/Application.ts | 40 +- packages/snjs/lib/Application/index.ts | 1 + packages/snjs/lib/Logging.ts | 22 + packages/snjs/lib/Migrations/Base.ts | 20 +- .../Migrations/StorageReaders/Functions.ts | 4 +- .../StorageReaders/Versions/Reader1_0_0.ts | 48 - .../StorageReaders/Versions/index.ts | 1 - .../snjs/lib/Migrations/Versions/2_0_0.ts | 730 ------------ .../snjs/lib/Migrations/Versions/index.ts | 12 +- .../Preferences/PreferencesService.ts | 2 +- .../Services/Singleton/SingletonManager.ts | 6 +- .../Services/Storage/DiskStorageService.ts | 12 +- .../snjs/lib/Services/Sync/SyncService.ts | 139 ++- packages/snjs/lib/Services/Sync/index.ts | 1 - packages/snjs/mocha/application.test.js | 4 +- packages/snjs/mocha/auth.test.js | 3 - .../snjs/mocha/key_recovery_service.test.js | 4 +- packages/snjs/mocha/lib/factory.js | 3 +- .../snjs/mocha/lib/web_device_interface.js | 71 +- .../migrations/2020-01-15-mobile.test.js | 1042 ----------------- .../mocha/migrations/2020-01-15-web.test.js | 584 --------- .../snjs/mocha/migrations/migration.test.js | 55 +- .../snjs/mocha/model_tests/importing.test.js | 2 +- .../snjs/mocha/model_tests/mapping.test.js | 2 +- packages/snjs/mocha/session.test.js | 4 +- packages/snjs/mocha/storage.test.js | 1 + .../snjs/mocha/sync_tests/offline.test.js | 5 +- packages/snjs/mocha/sync_tests/online.test.js | 32 +- packages/snjs/mocha/test.html | 2 - packages/snjs/package.json | 1 + .../javascripts/Application/Application.ts | 4 + .../src/javascripts/Application/Database.ts | 33 + .../Application/Device/WebOrDesktopDevice.ts | 82 +- yarn.lock | 11 + 59 files changed, 868 insertions(+), 3003 deletions(-) create mode 100644 .yarn/cache/react-native-mmkv-npm-2.5.1-4745a42823-6f0cf484e7.zip create mode 100644 packages/mobile/src/Lib/Database/Database.ts create mode 100644 packages/mobile/src/Lib/Database/DatabaseInterface.ts create mode 100644 packages/mobile/src/Lib/Database/DatabaseMetadata.ts create mode 100644 packages/mobile/src/Lib/Database/FlashKeyValueStore.ts create mode 100644 packages/mobile/src/Lib/Database/LegacyIdentifier.ts create mode 100644 packages/mobile/src/Lib/Database/LegacyKeyValueStore.ts create mode 100644 packages/mobile/src/Lib/Database/showLoadFailForItemIds.ts rename packages/mobile/src/Lib/{Interface.ts => MobileDevice.ts} (68%) create mode 100644 packages/services/src/Domain/Device/DatabaseItemMetadata.ts create mode 100644 packages/services/src/Domain/Device/DatabaseLoadOptions.ts rename packages/{snjs/lib/Services/Sync/Utils.spec.ts => services/src/Domain/Device/DatabaseLoadSorter.spec.ts} (90%) rename packages/{snjs/lib/Services/Sync/Utils.ts => services/src/Domain/Device/DatabaseLoadSorter.ts} (60%) create mode 100644 packages/snjs/lib/Logging.ts delete mode 100644 packages/snjs/lib/Migrations/StorageReaders/Versions/Reader1_0_0.ts delete mode 100644 packages/snjs/lib/Migrations/Versions/2_0_0.ts delete mode 100644 packages/snjs/mocha/migrations/2020-01-15-mobile.test.js delete mode 100644 packages/snjs/mocha/migrations/2020-01-15-web.test.js diff --git a/.yarn/cache/react-native-mmkv-npm-2.5.1-4745a42823-6f0cf484e7.zip b/.yarn/cache/react-native-mmkv-npm-2.5.1-4745a42823-6f0cf484e7.zip new file mode 100644 index 0000000000000000000000000000000000000000..5cefc0fcbd9f0d41c255822148e577bd3a6aee16 GIT binary patch literal 259912 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

Ftl0ApPf_M0OSlX%agI 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 MFC2i(Gl-R>f88;UYXATM literal 0 HcmV?d00001 diff --git a/packages/filepicker/example/src/web_device_interface.js b/packages/filepicker/example/src/web_device_interface.js index 810984731..6f5928107 100644 --- a/packages/filepicker/example/src/web_device_interface.js +++ b/packages/filepicker/example/src/web_device_interface.js @@ -18,17 +18,6 @@ export default class WebDeviceInterface { } } - async getAllRawStorageKeyValues() { - const results = [] - for (const key of Object.keys(localStorage)) { - results.push({ - key: key, - value: localStorage[key], - }) - } - return results - } - async setRawStorageValue(key, value) { localStorage.setItem(key, value) } @@ -57,7 +46,7 @@ export default class WebDeviceInterface { return `${this._getDatabaseKeyPrefix(identifier)}${id}` } - async getAllRawDatabasePayloads(identifier) { + async getAllDatabaseEntries(identifier) { const models = [] for (const key in localStorage) { if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) { @@ -67,21 +56,21 @@ export default class WebDeviceInterface { return models } - async saveRawDatabasePayload(payload, identifier) { + async saveDatabaseEntry(payload, identifier) { localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload)) } - async saveRawDatabasePayloads(payloads, identifier) { + async saveDatabaseEntries(payloads, identifier) { for (const payload of payloads) { - await this.saveRawDatabasePayload(payload, identifier) + await this.saveDatabaseEntry(payload, identifier) } } - async removeRawDatabasePayloadWithId(id, identifier) { + async removeDatabaseEntry(id, identifier) { localStorage.removeItem(this._keyForPayloadId(id, identifier)) } - async removeAllRawDatabasePayloads(identifier) { + async removeAllDatabaseEntries(identifier) { for (const key in localStorage) { if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) { delete localStorage[key] @@ -121,12 +110,6 @@ export default class WebDeviceInterface { localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain)) } - /** Allows unit tests to set legacy keychain structure as it was <= 003 */ - // eslint-disable-next-line camelcase - async setLegacyRawKeychainValue(value) { - localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value)) - } - async getRawKeychainValue() { const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY) return JSON.parse(keychain) diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 03697e2f5..af36a5f78 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -75,6 +75,9 @@ PODS: - glog (0.3.5) - hermes-engine (0.70.6) - libevent (2.1.12) + - MMKV (1.2.14): + - MMKVCore (~> 1.2.14) + - MMKVCore (1.2.14) - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): - boost @@ -304,6 +307,9 @@ PODS: - glog - react-native-fingerprint-scanner (5.0.0): - React-Core + - react-native-mmkv (2.5.1): + - MMKV (>= 1.2.13) + - React-Core - react-native-version-info (1.1.1): - React-Core - react-native-webview (11.23.1): @@ -444,6 +450,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`) + - react-native-mmkv (from `../node_modules/react-native-mmkv`) - react-native-version-info (from `../node_modules/react-native-version-info`) - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -483,6 +490,8 @@ SPEC REPOS: - FlipperKit - fmt - libevent + - MMKV + - MMKVCore - OpenSSL-Universal - SocketRocket - TrustKit @@ -533,6 +542,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-fingerprint-scanner: :path: "../node_modules/react-native-fingerprint-scanner" + react-native-mmkv: + :path: "../node_modules/react-native-mmkv" react-native-version-info: :path: "../node_modules/react-native-version-info" react-native-webview: @@ -583,7 +594,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 48289402952f4f7a4e235de70a9a590aa0b79ef4 FBReactNativeSpec: dd1186fd05255e3457baa2f4ca65e94c2cd1e3ac Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0 @@ -596,9 +607,11 @@ SPEC CHECKSUMS: Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541 FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 2af7b7a59128f250adfd86f15aa1d5a2ecd39995 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd + MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: e1866f61af7049eb3d8e08e8b133abd38bc1ca7a @@ -616,6 +629,7 @@ SPEC CHECKSUMS: React-jsinspector: 60769e5a0a6d4b32294a2456077f59d0266f9a8b React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e + react-native-mmkv: 69b9c003f10afdd01addf7c6ee784ce42ee2eff3 react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9 react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 diff --git a/packages/mobile/ios/StandardNotes/Info.plist b/packages/mobile/ios/StandardNotes/Info.plist index e67578d1b..e3d8d7255 100644 --- a/packages/mobile/ios/StandardNotes/Info.plist +++ b/packages/mobile/ios/StandardNotes/Info.plist @@ -100,7 +100,7 @@ NSPhotoLibraryUsageDescription Photo library is optionally used to select files to upload or QR code images from your photo library. NSMicrophoneUsageDescription - Microphone is optionally used to capture videos. + Microphone is optionally used to capture videos. UIAppFonts AntDesign.ttf @@ -147,5 +147,7 @@ supportsAlternateIcons + RCTAsyncStorageExcludeFromBackup + diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 5db3a0d7e..6bf8b0e8a 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -59,6 +59,7 @@ "react-native-fs": "^2.20.0", "react-native-iap": "^12.4.4", "react-native-keychain": "standardnotes/react-native-keychain#d277d360494cbd02be4accb4a360772a8e0e97b6", + "react-native-mmkv": "^2.5.1", "react-native-privacy-snapshot": "standardnotes/react-native-privacy-snapshot#653e904c90fc6f2b578da59138f2bfe5d7f942fe", "react-native-share": "^8.0.0", "react-native-version-info": "^1.1.1", diff --git a/packages/mobile/src/Lib/Database/Database.ts b/packages/mobile/src/Lib/Database/Database.ts new file mode 100644 index 000000000..5dfcb2ea5 --- /dev/null +++ b/packages/mobile/src/Lib/Database/Database.ts @@ -0,0 +1,158 @@ +import AsyncStorage from '@react-native-community/async-storage' +import { + DatabaseKeysLoadChunk, + DatabaseKeysLoadChunkResponse, + DatabaseLoadOptions, + GetSortedPayloadsByPriority, + TransferPayload, +} from '@standardnotes/snjs' +import { Platform } from 'react-native' +import { DatabaseInterface } from './DatabaseInterface' +import { DatabaseMetadata } from './DatabaseMetadata' +import { FlashKeyValueStore } from './FlashKeyValueStore' +import { isLegacyIdentifier } from './LegacyIdentifier' +import { showLoadFailForItemIds } from './showLoadFailForItemIds' + +export class Database implements DatabaseInterface { + private metadataStore: DatabaseMetadata + + constructor(private identifier: string) { + const flashStorage = new FlashKeyValueStore(identifier) + this.metadataStore = new DatabaseMetadata(identifier, flashStorage) + } + + private databaseKeyForPayloadId(id: string) { + return `${this.getDatabaseKeyPrefix()}${id}` + } + + private getDatabaseKeyPrefix() { + if (this.identifier && !isLegacyIdentifier(this.identifier)) { + return `${this.identifier}-Item-` + } else { + return 'Item-' + } + } + + async getAllEntries(): Promise { + const keys = await this.getAllKeys() + return this.multiGet(keys) + } + + async getAllKeys(): Promise { + const keys = await AsyncStorage.getAllKeys() + const filtered = keys.filter((key) => { + return key.startsWith(this.getDatabaseKeyPrefix()) + }) + return filtered + } + + async multiDelete(keys: string[]): Promise { + return AsyncStorage.multiRemove(keys) + } + + async deleteItem(itemUuid: string): Promise { + const key = this.databaseKeyForPayloadId(itemUuid) + this.metadataStore.deleteMetadataItem(itemUuid) + return this.multiDelete([key]) + } + + async deleteAll(): Promise { + const keys = await this.getAllKeys() + return this.multiDelete(keys) + } + + async setItems(items: TransferPayload[]): Promise { + if (items.length === 0) { + return + } + + await Promise.all( + items.map((item) => { + return Promise.all([ + AsyncStorage.setItem(this.databaseKeyForPayloadId(item.uuid), JSON.stringify(item)), + this.metadataStore.setMetadataForPayloads([item]), + ]) + }), + ) + } + + async getLoadChunks(options: DatabaseLoadOptions): Promise { + let metadataItems = this.metadataStore.getAllMetadataItems() + + if (metadataItems.length === 0) { + const allEntries = await this.getAllEntries() + metadataItems = this.metadataStore.runMigration(allEntries) + } + + const sorted = GetSortedPayloadsByPriority(metadataItems, options) + + const itemsKeysChunk: DatabaseKeysLoadChunk = { + keys: sorted.itemsKeyPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + } + + const contentTypePriorityChunk: DatabaseKeysLoadChunk = { + keys: sorted.contentTypePriorityPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + } + + const remainingKeys = sorted.remainingPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)) + + const remainingKeysChunks: DatabaseKeysLoadChunk[] = [] + for (let i = 0; i < remainingKeys.length; i += options.batchSize) { + remainingKeysChunks.push({ + keys: remainingKeys.slice(i, i + options.batchSize), + }) + } + + const result: DatabaseKeysLoadChunkResponse = { + keys: { + itemsKeys: itemsKeysChunk, + remainingChunks: [contentTypePriorityChunk, ...remainingKeysChunks], + }, + remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + } + + return result + } + + async multiGet(keys: string[]): Promise { + const results: T[] = [] + + if (Platform.OS === 'android') { + const failedItemIds: string[] = [] + for (const key of keys) { + try { + const item = await AsyncStorage.getItem(key) + if (item) { + try { + results.push(JSON.parse(item) as T) + } catch (e) { + results.push(item as T) + } + } + } catch (e) { + console.error('Error getting item', key, e) + failedItemIds.push(key) + } + } + if (failedItemIds.length > 0) { + showLoadFailForItemIds(failedItemIds) + } + } else { + try { + for (const item of await AsyncStorage.multiGet(keys)) { + if (item[1]) { + try { + results.push(JSON.parse(item[1])) + } catch (e) { + results.push(item[1] as T) + } + } + } + } catch (e) { + console.error('Error getting items', e) + } + } + + return results + } +} diff --git a/packages/mobile/src/Lib/Database/DatabaseInterface.ts b/packages/mobile/src/Lib/Database/DatabaseInterface.ts new file mode 100644 index 000000000..3840f8d2b --- /dev/null +++ b/packages/mobile/src/Lib/Database/DatabaseInterface.ts @@ -0,0 +1,10 @@ +import { TransferPayload } from '@standardnotes/snjs' + +export interface DatabaseInterface { + getAllKeys(): Promise + multiDelete(keys: string[]): Promise + deleteItem(itemUuid: string): Promise + deleteAll(): Promise + setItems(items: TransferPayload[]): Promise + multiGet(keys: string[]): Promise +} diff --git a/packages/mobile/src/Lib/Database/DatabaseMetadata.ts b/packages/mobile/src/Lib/Database/DatabaseMetadata.ts new file mode 100644 index 000000000..3434acfc6 --- /dev/null +++ b/packages/mobile/src/Lib/Database/DatabaseMetadata.ts @@ -0,0 +1,39 @@ +import { DatabaseItemMetadata, isNotUndefined, TransferPayload } from '@standardnotes/snjs' +import { FlashKeyValueStore } from './FlashKeyValueStore' + +export class DatabaseMetadata { + constructor(private identifier: string, private flashStorage: FlashKeyValueStore) {} + + runMigration(payloads: TransferPayload[]) { + const metadataItems = this.setMetadataForPayloads(payloads) + return metadataItems + } + + setMetadataForPayloads(payloads: TransferPayload[]) { + const metadataItems = [] + for (const payload of payloads) { + const { uuid, content_type, updated_at } = payload + const key = this.keyForUuid(uuid) + const metadata: DatabaseItemMetadata = { uuid, content_type, updated_at } + this.flashStorage.set(key, metadata) + metadataItems.push(metadata) + } + return metadataItems + } + + deleteMetadataItem(itemUuid: string) { + const key = this.keyForUuid(itemUuid) + this.flashStorage.delete(key) + } + + getAllMetadataItems(): DatabaseItemMetadata[] { + const keys = this.flashStorage.getAllKeys() + const metadataKeys = keys.filter((key) => key.endsWith('-Metadata')) + const metadataItems = this.flashStorage.multiGet(metadataKeys).filter(isNotUndefined) + return metadataItems + } + + private keyForUuid(uuid: string) { + return `${this.identifier}-Item-${uuid}-Metadata` + } +} diff --git a/packages/mobile/src/Lib/Database/FlashKeyValueStore.ts b/packages/mobile/src/Lib/Database/FlashKeyValueStore.ts new file mode 100644 index 000000000..f0e6afc1a --- /dev/null +++ b/packages/mobile/src/Lib/Database/FlashKeyValueStore.ts @@ -0,0 +1,40 @@ +import { MMKV } from 'react-native-mmkv' + +export class FlashKeyValueStore { + private storage: MMKV + + constructor(identifier: string) { + this.storage = new MMKV({ id: identifier }) + } + + set(key: string, value: unknown): void { + this.storage.set(key, JSON.stringify(value)) + } + + delete(key: string): void { + this.storage.delete(key) + } + + deleteAll(): void { + this.storage.clearAll() + } + + getAllKeys(): string[] { + return this.storage.getAllKeys() + } + + get(key: string): T | undefined { + const item = this.storage.getString(key) + if (item) { + try { + return JSON.parse(item) + } catch (e) { + return item as T + } + } + } + + multiGet(keys: string[]): (T | undefined)[] { + return keys.map((key) => this.get(key)) + } +} diff --git a/packages/mobile/src/Lib/Database/LegacyIdentifier.ts b/packages/mobile/src/Lib/Database/LegacyIdentifier.ts new file mode 100644 index 000000000..4d186507e --- /dev/null +++ b/packages/mobile/src/Lib/Database/LegacyIdentifier.ts @@ -0,0 +1,15 @@ +import { ApplicationIdentifier } from '@standardnotes/snjs' + +/** + * This identifier was the database name used in Standard Notes web/desktop. + */ +const LEGACY_IDENTIFIER = 'standardnotes' + +/** + * We use this function to decide if we need to prefix the identifier in getDatabaseKeyPrefix or not. + * It is also used to decide if the raw or the namespaced keychain is used. + * @param identifier The ApplicationIdentifier + */ +export const isLegacyIdentifier = function (identifier: ApplicationIdentifier) { + return identifier && identifier === LEGACY_IDENTIFIER +} diff --git a/packages/mobile/src/Lib/Database/LegacyKeyValueStore.ts b/packages/mobile/src/Lib/Database/LegacyKeyValueStore.ts new file mode 100644 index 000000000..e03d1f68c --- /dev/null +++ b/packages/mobile/src/Lib/Database/LegacyKeyValueStore.ts @@ -0,0 +1,26 @@ +import AsyncStorage from '@react-native-community/async-storage' + +export class LegacyKeyValueStore { + set(key: string, value: string): Promise { + return AsyncStorage.setItem(key, JSON.stringify(value)) + } + + delete(key: string): Promise { + return AsyncStorage.removeItem(key) + } + + deleteAll(): Promise { + return AsyncStorage.clear() + } + + async getValue(key: string): Promise { + const item = await AsyncStorage.getItem(key) + if (item) { + try { + return JSON.parse(item) + } catch (e) { + return item as T + } + } + } +} diff --git a/packages/mobile/src/Lib/Database/showLoadFailForItemIds.ts b/packages/mobile/src/Lib/Database/showLoadFailForItemIds.ts new file mode 100644 index 000000000..6406d2177 --- /dev/null +++ b/packages/mobile/src/Lib/Database/showLoadFailForItemIds.ts @@ -0,0 +1,16 @@ +import { Alert } from 'react-native' + +export const showLoadFailForItemIds = (failedItemIds: string[]) => { + let text = + 'The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. We recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n' + let index = 0 + text += failedItemIds.map((id) => { + let result = id + if (index !== failedItemIds.length - 1) { + result += '\n' + } + index++ + return result + }) + Alert.alert('Unable to load item(s)', text) +} diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/MobileDevice.ts similarity index 68% rename from packages/mobile/src/Lib/Interface.ts rename to packages/mobile/src/Lib/MobileDevice.ts index 60b31ad35..37ed3e77f 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -1,12 +1,11 @@ -import AsyncStorage from '@react-native-community/async-storage' import SNReactNative from '@standardnotes/react-native-utils' -import { AppleIAPReceipt } from '@standardnotes/services' import { AppleIAPProductId, + AppleIAPReceipt, ApplicationIdentifier, + DatabaseKeysLoadChunkResponse, + DatabaseLoadOptions, Environment, - LegacyMobileKeychainStructure, - LegacyRawKeychainValue, MobileDeviceInterface, NamespacedRootKeyInKeychain, Platform as SNPlatform, @@ -41,8 +40,11 @@ import { import { hide, show } from 'react-native-privacy-snapshot' import Share from 'react-native-share' import { AndroidBackHandlerService } from '../AndroidBackHandlerService' +import { AppStateObserverService } from '../AppStateObserverService' import { PurchaseManager } from '../PurchaseManager' -import { AppStateObserverService } from './../AppStateObserverService' +import { Database } from './Database/Database' +import { isLegacyIdentifier } from './Database/LegacyIdentifier' +import { LegacyKeyValueStore } from './Database/LegacyKeyValueStore' import Keychain from './Keychain' export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID' @@ -53,41 +55,6 @@ export enum MobileDeviceEvent { type MobileDeviceEventHandler = (event: MobileDeviceEvent) => void -/** - * This identifier was the database name used in Standard Notes web/desktop. - */ -const LEGACY_IDENTIFIER = 'standardnotes' - -/** - * We use this function to decide if we need to prefix the identifier in getDatabaseKeyPrefix or not. - * It is also used to decide if the raw or the namespaced keychain is used. - * @param identifier The ApplicationIdentifier - */ -const isLegacyIdentifier = function (identifier: ApplicationIdentifier) { - return identifier && identifier === LEGACY_IDENTIFIER -} - -function isLegacyMobileKeychain( - x: LegacyMobileKeychainStructure | RawKeychainValue, -): x is LegacyMobileKeychainStructure { - return x.ak != undefined -} - -const showLoadFailForItemIds = (failedItemIds: string[]) => { - let text = - 'The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. We recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n' - let index = 0 - text += failedItemIds.map((id) => { - let result = id - if (index !== failedItemIds.length - 1) { - result += '\n' - } - index++ - return result - }) - Alert.alert('Unable to load item(s)', text) -} - export class MobileDevice implements MobileDeviceInterface { environment: Environment.Mobile = Environment.Mobile platform: SNPlatform.Ios | SNPlatform.Android = Platform.OS === 'ios' ? SNPlatform.Ios : SNPlatform.Android @@ -95,6 +62,8 @@ export class MobileDevice implements MobileDeviceInterface { public isDarkMode = false public statusBarBgColor: string | undefined private componentUrls: Map = new Map() + private keyValueStore = new LegacyKeyValueStore() + private databases = new Map() constructor( private stateObserverService?: AppStateObserverService, @@ -106,6 +75,17 @@ export class MobileDevice implements MobileDeviceInterface { return PurchaseManager.getInstance().purchase(plan) } + private findOrCreateDatabase(identifier: ApplicationIdentifier): Database { + const existing = this.databases.get(identifier) + if (existing) { + return existing + } + + const newDb = new Database(identifier) + this.databases.set(identifier, newDb) + return newDb + } + deinit() { this.stateObserverService?.deinit() ;(this.stateObserverService as unknown) = undefined @@ -120,10 +100,6 @@ export class MobileDevice implements MobileDeviceInterface { console.log(args) } - async setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise { - await Keychain.setKeys(value) - } - public async getJsonParsedRawStorageValue(key: string): Promise { const value = await this.getRawStorageValue(key) if (value == undefined) { @@ -136,219 +112,57 @@ export class MobileDevice implements MobileDeviceInterface { } } - private getDatabaseKeyPrefix(identifier: ApplicationIdentifier) { - if (identifier && !isLegacyIdentifier(identifier)) { - return `${identifier}-Item-` - } else { - return 'Item-' - } - } - - private keyForPayloadId(id: string, identifier: ApplicationIdentifier) { - return `${this.getDatabaseKeyPrefix(identifier)}${id}` - } - - private async getAllDatabaseKeys(identifier: ApplicationIdentifier) { - const keys = await AsyncStorage.getAllKeys() - const filtered = keys.filter((key) => { - return key.startsWith(this.getDatabaseKeyPrefix(identifier)) - }) - return filtered - } - - getDatabaseKeys(): Promise { - return AsyncStorage.getAllKeys() - } - - private async getRawStorageKeyValues(keys: string[]) { - const results: { key: string; value: unknown }[] = [] - if (Platform.OS === 'android') { - for (const key of keys) { - try { - const item = await AsyncStorage.getItem(key) - if (item) { - results.push({ key, value: item }) - } - } catch (e) { - console.error('Error getting item', key, e) - } - } - } else { - try { - for (const item of await AsyncStorage.multiGet(keys)) { - if (item[1]) { - results.push({ key: item[0], value: item[1] }) - } - } - } catch (e) { - console.error('Error getting items', e) - } - } - return results - } - - private async getDatabaseKeyValues(keys: string[]) { - const results: (TransferPayload | unknown)[] = [] - - if (Platform.OS === 'android') { - const failedItemIds: string[] = [] - for (const key of keys) { - try { - const item = await AsyncStorage.getItem(key) - if (item) { - try { - results.push(JSON.parse(item) as TransferPayload) - } catch (e) { - results.push(item) - } - } - } catch (e) { - console.error('Error getting item', key, e) - failedItemIds.push(key) - } - } - if (failedItemIds.length > 0) { - showLoadFailForItemIds(failedItemIds) - } - } else { - try { - for (const item of await AsyncStorage.multiGet(keys)) { - if (item[1]) { - try { - results.push(JSON.parse(item[1])) - } catch (e) { - results.push(item[1]) - } - } - } - } catch (e) { - console.error('Error getting items', e) - } - } - return results - } - - async getRawStorageValue(key: string) { - const item = await AsyncStorage.getItem(key) - if (item) { - try { - return JSON.parse(item) - } catch (e) { - return item - } - } - } - - hideMobileInterfaceFromScreenshots(): void { - hide() - this.setAndroidScreenshotPrivacy(true) - } - - stopHidingMobileInterfaceFromScreenshots(): void { - show() - this.setAndroidScreenshotPrivacy(false) - } - - async getAllRawStorageKeyValues() { - const keys = await AsyncStorage.getAllKeys() - return this.getRawStorageKeyValues(keys) + getRawStorageValue(key: string): Promise { + return this.keyValueStore.getValue(key) } setRawStorageValue(key: string, value: string): Promise { - return AsyncStorage.setItem(key, JSON.stringify(value)) + return this.keyValueStore.set(key, value) } removeRawStorageValue(key: string): Promise { - return AsyncStorage.removeItem(key) + return this.keyValueStore.delete(key) } removeAllRawStorageValues(): Promise { - return AsyncStorage.clear() + return this.keyValueStore.deleteAll() } openDatabase(): Promise<{ isNewDatabase?: boolean | undefined } | undefined> { return Promise.resolve({ isNewDatabase: false }) } - async getAllRawDatabasePayloads( + getDatabaseLoadChunks(options: DatabaseLoadOptions, identifier: string): Promise { + return this.findOrCreateDatabase(identifier).getLoadChunks(options) + } + + async getAllDatabaseEntries( identifier: ApplicationIdentifier, ): Promise { - const keys = await this.getAllDatabaseKeys(identifier) - return this.getDatabaseKeyValues(keys) as Promise + return this.findOrCreateDatabase(identifier).getAllEntries() } - saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise { - return this.saveRawDatabasePayloads([payload], identifier) - } - - async saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise { - if (payloads.length === 0) { - return - } - await Promise.all( - payloads.map((item) => { - return AsyncStorage.setItem(this.keyForPayloadId(item.uuid, identifier), JSON.stringify(item)) - }), - ) - } - - removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise { - return this.removeRawStorageValue(this.keyForPayloadId(id, identifier)) - } - - async removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise { - const keys = await this.getAllDatabaseKeys(identifier) - return AsyncStorage.multiRemove(keys) - } - - async getNamespacedKeychainValue( + async getDatabaseEntries( identifier: ApplicationIdentifier, - ): Promise { - const keychain = await this.getRawKeychainValue() - - if (!keychain) { - return - } - - const namespacedValue = keychain[identifier] - - if (!namespacedValue && isLegacyIdentifier(identifier)) { - return keychain as unknown as NamespacedRootKeyInKeychain - } - - return namespacedValue + keys: string[], + ): Promise { + return this.findOrCreateDatabase(identifier).multiGet(keys) } - async setNamespacedKeychainValue( - value: NamespacedRootKeyInKeychain, - identifier: ApplicationIdentifier, - ): Promise { - let keychain = await this.getRawKeychainValue() - - if (!keychain) { - keychain = {} - } - - await Keychain.setKeys({ - ...keychain, - [identifier]: value, - }) + saveDatabaseEntry(payload: TransferPayload, identifier: ApplicationIdentifier): Promise { + return this.saveDatabaseEntries([payload], identifier) } - async clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise { - const keychain = await this.getRawKeychainValue() + async saveDatabaseEntries(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise { + return this.findOrCreateDatabase(identifier).setItems(payloads) + } - if (!keychain) { - return - } + removeDatabaseEntry(id: string, identifier: ApplicationIdentifier): Promise { + return this.findOrCreateDatabase(identifier).deleteItem(id) + } - if (!keychain[identifier] && isLegacyIdentifier(identifier) && isLegacyMobileKeychain(keychain)) { - await this.clearRawKeychainValue() - return - } - - delete keychain[identifier] - await Keychain.setKeys(keychain) + async removeAllDatabaseEntries(identifier: ApplicationIdentifier): Promise { + return this.findOrCreateDatabase(identifier).deleteAll() } async getDeviceBiometricsAvailability() { @@ -413,6 +227,51 @@ export class MobileDevice implements MobileDeviceInterface { return result } + async getNamespacedKeychainValue( + identifier: ApplicationIdentifier, + ): Promise { + const keychain = await this.getRawKeychainValue() + + if (!keychain) { + return + } + + const namespacedValue = keychain[identifier] + + if (!namespacedValue && isLegacyIdentifier(identifier)) { + return keychain as unknown as NamespacedRootKeyInKeychain + } + + return namespacedValue + } + + async setNamespacedKeychainValue( + value: NamespacedRootKeyInKeychain, + identifier: ApplicationIdentifier, + ): Promise { + let keychain = await this.getRawKeychainValue() + + if (!keychain) { + keychain = {} + } + + await Keychain.setKeys({ + ...keychain, + [identifier]: value, + }) + } + + async clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise { + const keychain = await this.getRawKeychainValue() + + if (!keychain) { + return + } + + delete keychain[identifier] + await Keychain.setKeys(keychain) + } + async getRawKeychainValue(): Promise { const result = await Keychain.getKeys() @@ -641,4 +500,14 @@ export class MobileDevice implements MobileDeviceInterface { async getColorScheme(): Promise { return Appearance.getColorScheme() } + + hideMobileInterfaceFromScreenshots(): void { + hide() + this.setAndroidScreenshotPrivacy(true) + } + + stopHidingMobileInterfaceFromScreenshots(): void { + show() + this.setAndroidScreenshotPrivacy(false) + } } diff --git a/packages/mobile/src/MobileWebAppContainer.tsx b/packages/mobile/src/MobileWebAppContainer.tsx index 0dc2cb828..bc4a1ec21 100644 --- a/packages/mobile/src/MobileWebAppContainer.tsx +++ b/packages/mobile/src/MobileWebAppContainer.tsx @@ -7,7 +7,7 @@ import { OnShouldStartLoadWithRequest } from 'react-native-webview/lib/WebViewTy import { AndroidBackHandlerService } from './AndroidBackHandlerService' import { AppStateObserverService } from './AppStateObserverService' import { ColorSchemeObserverService } from './ColorSchemeObserverService' -import { MobileDevice, MobileDeviceEvent } from './Lib/Interface' +import { MobileDevice, MobileDeviceEvent } from './Lib/MobileDevice' import { IsDev } from './Lib/Utils' const LoggingEnabled = IsDev @@ -177,6 +177,10 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo window.ReactNativeWebView.postMessage('[web log] ' + args.join(' ')); } + console.error = (...args) => { + window.ReactNativeWebView.postMessage('[web log] ' + args.join(' ')); + } + ${WebProcessDeviceInterface} ${WebProcessMessageSender} diff --git a/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts index 9e5a12bd7..97cc09f40 100644 --- a/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts +++ b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts @@ -10,22 +10,3 @@ export interface NamespacedRootKeyInKeychain { } export type RootKeyContentInStorage = RootKeyContentSpecialized - -export interface LegacyRawKeychainValue { - mk: string - ak: string - version: ProtocolVersion -} - -export type LegacyMobileKeychainStructure = { - offline?: { - timing?: unknown - pw?: string - } - encryptedAccountKeys?: unknown - mk: string - pw: string - ak: string - version?: string - jwt?: string -} diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 84afe8fdb..1538c8bc2 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -1,7 +1,6 @@ import { ApplicationIdentifier, ContentType } from '@standardnotes/common' import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models' import { FilesClientInterface } from '@standardnotes/files' - import { AlertService } from '../Alert/AlertService' import { ComponentManagerInterface } from '../Component/ComponentManagerInterface' import { ApplicationEvent } from '../Event/ApplicationEvent' diff --git a/packages/services/src/Domain/Device/DatabaseItemMetadata.ts b/packages/services/src/Domain/Device/DatabaseItemMetadata.ts new file mode 100644 index 000000000..af3b77a8e --- /dev/null +++ b/packages/services/src/Domain/Device/DatabaseItemMetadata.ts @@ -0,0 +1,3 @@ +import { TransferPayload } from '@standardnotes/models' + +export type DatabaseItemMetadata = Pick diff --git a/packages/services/src/Domain/Device/DatabaseLoadOptions.ts b/packages/services/src/Domain/Device/DatabaseLoadOptions.ts new file mode 100644 index 000000000..daad481b8 --- /dev/null +++ b/packages/services/src/Domain/Device/DatabaseLoadOptions.ts @@ -0,0 +1,44 @@ +import { ContentType } from '@standardnotes/common' +import { FullyFormedTransferPayload } from '@standardnotes/models' + +export type DatabaseKeysLoadChunk = { + keys: string[] +} + +export type DatabaseFullEntryLoadChunk = { + entries: FullyFormedTransferPayload[] +} + +export function isChunkFullEntry( + x: DatabaseKeysLoadChunk | DatabaseFullEntryLoadChunk, +): x is DatabaseFullEntryLoadChunk { + return (x as DatabaseFullEntryLoadChunk).entries !== undefined +} + +export type DatabaseKeysLoadChunkResponse = { + keys: { + itemsKeys: DatabaseKeysLoadChunk + remainingChunks: DatabaseKeysLoadChunk[] + } + remainingChunksItemCount: number +} + +export type DatabaseFullEntryLoadChunkResponse = { + fullEntries: { + itemsKeys: DatabaseFullEntryLoadChunk + remainingChunks: DatabaseFullEntryLoadChunk[] + } + remainingChunksItemCount: number +} + +export function isFullEntryLoadChunkResponse( + x: DatabaseKeysLoadChunkResponse | DatabaseFullEntryLoadChunkResponse, +): x is DatabaseFullEntryLoadChunkResponse { + return (x as DatabaseFullEntryLoadChunkResponse).fullEntries !== undefined +} + +export type DatabaseLoadOptions = { + contentTypePriority: ContentType[] + uuidPriority: string[] + batchSize: number +} diff --git a/packages/snjs/lib/Services/Sync/Utils.spec.ts b/packages/services/src/Domain/Device/DatabaseLoadSorter.spec.ts similarity index 90% rename from packages/snjs/lib/Services/Sync/Utils.spec.ts rename to packages/services/src/Domain/Device/DatabaseLoadSorter.spec.ts index 0b3490c72..d52f4167c 100644 --- a/packages/snjs/lib/Services/Sync/Utils.spec.ts +++ b/packages/services/src/Domain/Device/DatabaseLoadSorter.spec.ts @@ -1,6 +1,6 @@ import { ContentType } from '@standardnotes/common' import { FullyFormedPayloadInterface } from '@standardnotes/models' -import { GetSortedPayloadsByPriority } from './Utils' +import { GetSortedPayloadsByPriority } from './DatabaseLoadSorter' describe('GetSortedPayloadsByPriority', () => { let payloads: FullyFormedPayloadInterface[] = [] @@ -26,11 +26,11 @@ describe('GetSortedPayloadsByPriority', () => { } as FullyFormedPayloadInterface, ] - const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( - payloads, + const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(payloads, { contentTypePriority, - launchPriorityUuids, - ) + uuidPriority: launchPriorityUuids, + batchSize: 1000, + }) expect(itemsKeyPayloads.length).toBe(1) expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey) @@ -84,11 +84,11 @@ describe('GetSortedPayloadsByPriority', () => { launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid] - const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( - payloads, + const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority(payloads, { contentTypePriority, - launchPriorityUuids, - ) + uuidPriority: launchPriorityUuids, + batchSize: 1000, + }) expect(itemsKeyPayloads.length).toBe(1) expect(itemsKeyPayloads[0].content_type).toBe(ContentType.ItemsKey) @@ -116,12 +116,12 @@ describe('GetSortedPayloadsByPriority', () => { { content_type: ContentType.Note, uuid: unprioritizedNoteUuid, - serverUpdatedAt: new Date(1), + updated_at: new Date(1), } as FullyFormedPayloadInterface, { content_type: ContentType.Tag, uuid: unprioritizedTagUuid, - serverUpdatedAt: new Date(2), + updated_at: new Date(2), } as FullyFormedPayloadInterface, { content_type: ContentType.Note, @@ -135,7 +135,11 @@ describe('GetSortedPayloadsByPriority', () => { launchPriorityUuids = [prioritizedNoteUuid, prioritizedTagUuid] - const { remainingPayloads } = GetSortedPayloadsByPriority(payloads, contentTypePriority, launchPriorityUuids) + const { remainingPayloads } = GetSortedPayloadsByPriority(payloads, { + contentTypePriority, + uuidPriority: launchPriorityUuids, + batchSize: 1000, + }) expect(remainingPayloads.length).toBe(4) expect(remainingPayloads[0].uuid).toBe(prioritizedNoteUuid) diff --git a/packages/snjs/lib/Services/Sync/Utils.ts b/packages/services/src/Domain/Device/DatabaseLoadSorter.ts similarity index 60% rename from packages/snjs/lib/Services/Sync/Utils.ts rename to packages/services/src/Domain/Device/DatabaseLoadSorter.ts index cf917eb4d..12a05fd63 100644 --- a/packages/snjs/lib/Services/Sync/Utils.ts +++ b/packages/services/src/Domain/Device/DatabaseLoadSorter.ts @@ -1,18 +1,18 @@ -import { UuidString } from '@Lib/Types' -import { ContentType } from '@standardnotes/common' -import { FullyFormedPayloadInterface } from '@standardnotes/models' +import { DatabaseItemMetadata } from './DatabaseItemMetadata' +import { DatabaseLoadOptions } from './DatabaseLoadOptions' +import { ContentType, Uuid } from '@standardnotes/common' /** * Sorts payloads according by most recently modified first, according to the priority, * whereby the earlier a content_type appears in the priorityList, * the earlier it will appear in the resulting sorted array. */ -function SortPayloadsByRecentAndContentPriority( - payloads: FullyFormedPayloadInterface[], +function SortPayloadsByRecentAndContentPriority( + payloads: T[], contentTypePriorityList: ContentType[], -): FullyFormedPayloadInterface[] { +): T[] { return payloads.sort((a, b) => { - const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime() + const dateResult = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() let aPriority = 0 let bPriority = 0 @@ -45,12 +45,12 @@ function SortPayloadsByRecentAndContentPriority( * whereby the earlier a uuid appears in the priorityList, * the earlier it will appear in the resulting sorted array. */ -function SortPayloadsByRecentAndUuidPriority( - payloads: FullyFormedPayloadInterface[], - uuidPriorityList: UuidString[], -): FullyFormedPayloadInterface[] { +function SortPayloadsByRecentAndUuidPriority( + payloads: T[], + uuidPriorityList: Uuid[], +): T[] { return payloads.sort((a, b) => { - const dateResult = new Date(b.serverUpdatedAt).getTime() - new Date(a.serverUpdatedAt).getTime() + const dateResult = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() let aPriority = 0 let bPriority = 0 @@ -78,25 +78,24 @@ function SortPayloadsByRecentAndUuidPriority( }) } -export function GetSortedPayloadsByPriority( - payloads: FullyFormedPayloadInterface[], - contentTypePriorityList: ContentType[], - uuidPriorityList: UuidString[], +export function GetSortedPayloadsByPriority( + payloads: T[], + options: DatabaseLoadOptions, ): { - itemsKeyPayloads: FullyFormedPayloadInterface[] - contentTypePriorityPayloads: FullyFormedPayloadInterface[] - remainingPayloads: FullyFormedPayloadInterface[] + itemsKeyPayloads: T[] + contentTypePriorityPayloads: T[] + remainingPayloads: T[] } { - const itemsKeyPayloads: FullyFormedPayloadInterface[] = [] - const contentTypePriorityPayloads: FullyFormedPayloadInterface[] = [] - const remainingPayloads: FullyFormedPayloadInterface[] = [] + const itemsKeyPayloads: T[] = [] + const contentTypePriorityPayloads: T[] = [] + const remainingPayloads: T[] = [] for (let index = 0; index < payloads.length; index++) { const payload = payloads[index] if (payload.content_type === ContentType.ItemsKey) { itemsKeyPayloads.push(payload) - } else if (contentTypePriorityList.includes(payload.content_type)) { + } else if (options.contentTypePriority.includes(payload.content_type)) { contentTypePriorityPayloads.push(payload) } else { remainingPayloads.push(payload) @@ -107,8 +106,8 @@ export function GetSortedPayloadsByPriority( itemsKeyPayloads, contentTypePriorityPayloads: SortPayloadsByRecentAndContentPriority( contentTypePriorityPayloads, - contentTypePriorityList, + options.contentTypePriority, ), - remainingPayloads: SortPayloadsByRecentAndUuidPriority(remainingPayloads, uuidPriorityList), + remainingPayloads: SortPayloadsByRecentAndUuidPriority(remainingPayloads, options.uuidPriority), } } diff --git a/packages/services/src/Domain/Device/DeviceInterface.ts b/packages/services/src/Domain/Device/DeviceInterface.ts index 8ee94424f..f4ef66e26 100644 --- a/packages/services/src/Domain/Device/DeviceInterface.ts +++ b/packages/services/src/Domain/Device/DeviceInterface.ts @@ -2,10 +2,14 @@ import { ApplicationIdentifier } from '@standardnotes/common' import { FullyFormedTransferPayload, TransferPayload, - LegacyRawKeychainValue, NamespacedRootKeyInKeychain, Environment, } from '@standardnotes/models' +import { + DatabaseLoadOptions, + DatabaseKeysLoadChunkResponse, + DatabaseFullEntryLoadChunkResponse, +} from './DatabaseLoadOptions' /** * Platforms must override this class to provide platform specific utilities @@ -21,8 +25,6 @@ export interface DeviceInterface { getJsonParsedRawStorageValue(key: string): Promise - getAllRawStorageKeyValues(): Promise<{ key: string; value: unknown }[]> - setRawStorageValue(key: string, value: string): Promise removeRawStorageValue(key: string): Promise @@ -38,10 +40,10 @@ export interface DeviceInterface { */ openDatabase(identifier: ApplicationIdentifier): Promise<{ isNewDatabase?: boolean } | undefined> - /** - * In a key/value database, this function returns just the keys. - */ - getDatabaseKeys(): Promise + getDatabaseLoadChunks( + options: DatabaseLoadOptions, + identifier: ApplicationIdentifier, + ): Promise /** * Remove all keychain and database data from device. @@ -52,17 +54,22 @@ export interface DeviceInterface { */ clearAllDataFromDevice(workspaceIdentifiers: ApplicationIdentifier[]): Promise<{ killsApplication: boolean }> - getAllRawDatabasePayloads( + getAllDatabaseEntries( identifier: ApplicationIdentifier, ): Promise - saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise + getDatabaseEntries( + identifier: ApplicationIdentifier, + keys: string[], + ): Promise - saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise + saveDatabaseEntry(payload: TransferPayload, identifier: ApplicationIdentifier): Promise - removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise + saveDatabaseEntries(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise - removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise + removeDatabaseEntry(id: string, identifier: ApplicationIdentifier): Promise + + removeAllDatabaseEntries(identifier: ApplicationIdentifier): Promise getNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise @@ -70,8 +77,6 @@ export interface DeviceInterface { clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise - setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise - clearRawKeychainValue(): Promise openUrl(url: string): void diff --git a/packages/services/src/Domain/Storage/StorageServiceInterface.ts b/packages/services/src/Domain/Storage/StorageServiceInterface.ts index 9faba6ce2..1663f62f0 100644 --- a/packages/services/src/Domain/Storage/StorageServiceInterface.ts +++ b/packages/services/src/Domain/Storage/StorageServiceInterface.ts @@ -1,7 +1,13 @@ -import { FullyFormedPayloadInterface, PayloadInterface, RootKeyInterface } from '@standardnotes/models' +import { + FullyFormedPayloadInterface, + PayloadInterface, + RootKeyInterface, + FullyFormedTransferPayload, +} from '@standardnotes/models' import { StoragePersistencePolicies, StorageValueModes } from './StorageTypes' export interface StorageServiceInterface { + getAllRawPayloads(): Promise getValue(key: string, mode?: StorageValueModes, defaultValue?: T): T canDecryptWithKey(key: RootKeyInterface): Promise savePayload(payload: PayloadInterface): Promise diff --git a/packages/services/src/Domain/Sync/SyncOptions.ts b/packages/services/src/Domain/Sync/SyncOptions.ts index abc44c983..96e0352e3 100644 --- a/packages/services/src/Domain/Sync/SyncOptions.ts +++ b/packages/services/src/Domain/Sync/SyncOptions.ts @@ -11,6 +11,7 @@ export type SyncOptions = { checkIntegrity?: boolean /** Internally used to keep track of how sync requests were spawned. */ source: SyncSource + sourceDescription?: string /** Whether to await any sync requests that may be queued from this call. */ awaitAll?: boolean /** diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index cb1ffdda7..e770eb4c3 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -22,6 +22,9 @@ export * from './Device/DeviceInterface' export * from './Device/MobileDeviceInterface' export * from './Device/TypeCheck' export * from './Device/WebOrDesktopDeviceInterface' +export * from './Device/DatabaseLoadOptions' +export * from './Device/DatabaseItemMetadata' +export * from './Device/DatabaseLoadSorter' export * from './Diagnostics/ServiceDiagnostics' export * from './Encryption/BackupFileDecryptor' export * from './Encryption/EncryptionService' diff --git a/packages/snjs/lib/Application/Application.spec.ts b/packages/snjs/lib/Application/Application.spec.ts index 8381b2c0f..192d9902c 100644 --- a/packages/snjs/lib/Application/Application.spec.ts +++ b/packages/snjs/lib/Application/Application.spec.ts @@ -25,7 +25,7 @@ describe('application', () => { device = {} as jest.Mocked device.openDatabase = jest.fn().mockResolvedValue(true) - device.getAllRawDatabasePayloads = jest.fn().mockReturnValue([]) + device.getAllDatabaseEntries = jest.fn().mockReturnValue([]) device.setRawStorageValue = jest.fn() device.getRawStorageValue = jest.fn().mockImplementation((key) => { if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) { @@ -33,9 +33,6 @@ describe('application', () => { } return undefined }) - device.getDatabaseKeys = async () => { - return Promise.resolve(['1', '2', '3']) - } application = new SNApplication({ environment: Environment.Mobile, @@ -75,7 +72,6 @@ describe('application', () => { currentPersistPromise: false, isStorageWrapped: false, allRawPayloadsCount: 0, - databaseKeys: ['1', '2', '3'], }, encryption: expect.objectContaining({ getLatestVersion: '004', diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 61e377c5c..903b74b05 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -410,28 +410,32 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli await this.notifyEvent(ApplicationEvent.Launched) await this.handleStage(ExternalServices.ApplicationStage.Launched_10) - const databasePayloads = await this.syncService.getDatabasePayloads() await this.handleStage(ExternalServices.ApplicationStage.LoadingDatabase_11) - if (this.createdNewDatabase) { await this.syncService.onNewDatabaseCreated() } /** * We don't want to await this, as we want to begin allowing the app to function - * before local data has been loaded fully. We await only initial - * `getDatabasePayloads` to lock in on database state. + * before local data has been loaded fully. */ - const loadPromise = this.syncService.loadDatabasePayloads(databasePayloads).then(async () => { - if (this.dealloced) { - throw 'Application has been destroyed.' - } - await this.handleStage(ExternalServices.ApplicationStage.LoadedDatabase_12) - this.beginAutoSyncTimer() - await this.syncService.sync({ - mode: ExternalServices.SyncMode.DownloadFirst, - source: ExternalServices.SyncSource.External, + const loadPromise = this.syncService + .loadDatabasePayloads() + .then(async () => { + if (this.dealloced) { + throw 'Application has been destroyed.' + } + await this.handleStage(ExternalServices.ApplicationStage.LoadedDatabase_12) + this.beginAutoSyncTimer() + await this.syncService.sync({ + mode: ExternalServices.SyncMode.DownloadFirst, + source: ExternalServices.SyncSource.External, + sourceDescription: 'Application Launch', + }) + }) + .catch((error) => { + void this.notifyEvent(ApplicationEvent.LocalDatabaseReadError, error) + throw error }) - }) if (awaitDatabaseLoad) { await loadPromise } @@ -463,7 +467,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private beginAutoSyncTimer() { this.autoSyncInterval = setInterval(() => { this.syncService.log('Syncing from autosync') - void this.sync.sync() + void this.sync.sync({ sourceDescription: 'Auto Sync' }) }, DEFAULT_AUTO_SYNC_INTERVAL) } @@ -1542,10 +1546,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli switch (event) { case InternalServices.SessionEvent.Restored: { void (async () => { - await this.sync.sync() + await this.sync.sync({ sourceDescription: 'Session restored pre key creation' }) if (this.protocolService.needsNewRootKeyBasedItemsKey()) { void this.protocolService.createNewDefaultItemsKey().then(() => { - void this.sync.sync() + void this.sync.sync({ sourceDescription: 'Session restored post key creation' }) }) } })() @@ -1573,6 +1577,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.payloadManager, this.apiService, this.historyManager, + this.deviceInterface, + this.identifier, { loadBatchSize: this.options.loadBatchSize, }, diff --git a/packages/snjs/lib/Application/index.ts b/packages/snjs/lib/Application/index.ts index 29e6e3594..ea875adf3 100644 --- a/packages/snjs/lib/Application/index.ts +++ b/packages/snjs/lib/Application/index.ts @@ -2,3 +2,4 @@ export * from './Application' export * from './Event' export * from './LiveItem' export * from './Platforms' +export * from './Options/Defaults' diff --git a/packages/snjs/lib/Logging.ts b/packages/snjs/lib/Logging.ts new file mode 100644 index 000000000..b19375a09 --- /dev/null +++ b/packages/snjs/lib/Logging.ts @@ -0,0 +1,22 @@ +import { log as utilsLog } from '@standardnotes/utils' + +export const isDev = true + +export enum LoggingDomain { + DatabaseLoad, + Sync, +} + +const LoggingStatus: Record = { + [LoggingDomain.DatabaseLoad]: false, + [LoggingDomain.Sync]: false, +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function log(domain: LoggingDomain, ...args: any[]): void { + if (!isDev || !LoggingStatus[domain]) { + return + } + + utilsLog(LoggingDomain[domain], ...args) +} diff --git a/packages/snjs/lib/Migrations/Base.ts b/packages/snjs/lib/Migrations/Base.ts index 13f71a4f3..4216f2a5e 100644 --- a/packages/snjs/lib/Migrations/Base.ts +++ b/packages/snjs/lib/Migrations/Base.ts @@ -165,12 +165,11 @@ export class BaseMigration extends Migration { } private async repairMissingKeychain() { - const version = (await this.getStoredVersion()) as string const rawAccountParams = await this.reader.getAccountKeyParams() /** Choose an item to decrypt against */ const allItems = ( - await this.services.deviceInterface.getAllRawDatabasePayloads(this.services.identifier) + await this.services.deviceInterface.getAllDatabaseEntries(this.services.identifier) ).map((p) => new EncryptedPayload(p)) let itemToDecrypt = allItems.find((item) => { @@ -226,21 +225,10 @@ export class BaseMigration extends Migration { ) } else { /** - * If decryption succeeds, store the generated account key where it is expected, - * either in top-level keychain in 1.0.0, and namespaced location in 2.0.0+. + * If decryption succeeds, store the generated account key where it is expected. */ - if (version === PreviousSnjsVersion1_0_0) { - /** Store in top level keychain */ - await this.services.deviceInterface.setLegacyRawKeychainValue({ - mk: rootKey.masterKey, - ak: rootKey.dataAuthenticationKey as string, - version: accountParams.version, - }) - } else { - /** Store in namespaced location */ - const rawKey = rootKey.getKeychainValue() - await this.services.deviceInterface.setNamespacedKeychainValue(rawKey, this.services.identifier) - } + const rawKey = rootKey.getKeychainValue() + await this.services.deviceInterface.setNamespacedKeychainValue(rawKey, this.services.identifier) resolve(true) this.services.challengeService.completeChallenge(challenge) } diff --git a/packages/snjs/lib/Migrations/StorageReaders/Functions.ts b/packages/snjs/lib/Migrations/StorageReaders/Functions.ts index a637ca2a2..62b4228ba 100644 --- a/packages/snjs/lib/Migrations/StorageReaders/Functions.ts +++ b/packages/snjs/lib/Migrations/StorageReaders/Functions.ts @@ -5,9 +5,7 @@ import { DeviceInterface } from '@standardnotes/services' import { StorageReader } from './Reader' import * as ReaderClasses from './Versions' -function ReaderClassForVersion( - version: string, -): typeof ReaderClasses.StorageReader2_0_0 | typeof ReaderClasses.StorageReader1_0_0 { +function ReaderClassForVersion(version: string): typeof ReaderClasses.StorageReader2_0_0 { /** Sort readers by newest first */ const allReaders = Object.values(ReaderClasses).sort((a, b) => { return compareSemVersions(a.version(), b.version()) * -1 diff --git a/packages/snjs/lib/Migrations/StorageReaders/Versions/Reader1_0_0.ts b/packages/snjs/lib/Migrations/StorageReaders/Versions/Reader1_0_0.ts deleted file mode 100644 index 24ab4a212..000000000 --- a/packages/snjs/lib/Migrations/StorageReaders/Versions/Reader1_0_0.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isNullOrUndefined } from '@standardnotes/utils' -import { isEnvironmentMobile } from '@Lib/Application/Platforms' -import { PreviousSnjsVersion1_0_0 } from '../../../Version' -import { isMobileDevice, LegacyKeys1_0_0 } from '@standardnotes/services' -import { StorageReader } from '../Reader' - -export class StorageReader1_0_0 extends StorageReader { - static override version() { - return PreviousSnjsVersion1_0_0 - } - - public async getAccountKeyParams() { - return this.deviceInterface.getJsonParsedRawStorageValue(LegacyKeys1_0_0.AllAccountKeyParamsKey) - } - - /** - * In 1.0.0, web uses raw storage for unwrapped account key, and mobile uses - * the keychain - */ - public async hasNonWrappedAccountKeys() { - if (isMobileDevice(this.deviceInterface)) { - const value = await this.deviceInterface.getRawKeychainValue() - return !isNullOrUndefined(value) - } else { - const value = await this.deviceInterface.getRawStorageValue('mk') - return !isNullOrUndefined(value) - } - } - - public async hasPasscode() { - if (isEnvironmentMobile(this.environment)) { - const rawPasscodeParams = await this.deviceInterface.getJsonParsedRawStorageValue( - LegacyKeys1_0_0.MobilePasscodeParamsKey, - ) - return !isNullOrUndefined(rawPasscodeParams) - } else { - const encryptedStorage = await this.deviceInterface.getJsonParsedRawStorageValue( - LegacyKeys1_0_0.WebEncryptedStorageKey, - ) - return !isNullOrUndefined(encryptedStorage) - } - } - - /** Keychain was not used on desktop/web in 1.0.0 */ - public usesKeychain() { - return isEnvironmentMobile(this.environment) ? true : false - } -} diff --git a/packages/snjs/lib/Migrations/StorageReaders/Versions/index.ts b/packages/snjs/lib/Migrations/StorageReaders/Versions/index.ts index 68c37d73d..a9225a9e1 100644 --- a/packages/snjs/lib/Migrations/StorageReaders/Versions/index.ts +++ b/packages/snjs/lib/Migrations/StorageReaders/Versions/index.ts @@ -1,2 +1 @@ export { StorageReader2_0_0 } from './Reader2_0_0' -export { StorageReader1_0_0 } from './Reader1_0_0' diff --git a/packages/snjs/lib/Migrations/Versions/2_0_0.ts b/packages/snjs/lib/Migrations/Versions/2_0_0.ts deleted file mode 100644 index ec032f7c8..000000000 --- a/packages/snjs/lib/Migrations/Versions/2_0_0.ts +++ /dev/null @@ -1,730 +0,0 @@ -import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common' -import { Migration } from '@Lib/Migrations/Migration' -import { MigrationServices } from '../MigrationServices' -import { PreviousSnjsVersion2_0_0 } from '../../Version' -import { SNRootKey, CreateNewRootKey } from '@standardnotes/encryption' -import { DiskStorageService } from '../../Services/Storage/DiskStorageService' -import { StorageReader1_0_0 } from '../StorageReaders/Versions/Reader1_0_0' -import * as Models from '@standardnotes/models' -import * as Services from '@standardnotes/services' -import * as Utils from '@standardnotes/utils' -import { isEnvironmentMobile, isEnvironmentWebOrDesktop } from '@Lib/Application/Platforms' -import { - getIncrementedDirtyIndex, - LegacyMobileKeychainStructure, - PayloadTimestampDefaults, -} from '@standardnotes/models' -import { isMobileDevice } from '@standardnotes/services' -import { LegacySession } from '@standardnotes/domain-core' - -interface LegacyStorageContent extends Models.ItemContent { - storage: unknown -} - -interface LegacyAccountKeysValue { - ak: string - mk: string - version: string - jwt: string -} - -interface LegacyRootKeyContent extends Models.RootKeyContent { - accountKeys?: LegacyAccountKeysValue -} - -const LEGACY_SESSION_TOKEN_KEY = 'jwt' - -export class Migration2_0_0 extends Migration { - private legacyReader!: StorageReader1_0_0 - - constructor(services: MigrationServices) { - super(services) - this.legacyReader = new StorageReader1_0_0( - this.services.deviceInterface, - this.services.identifier, - this.services.environment, - ) - } - - static override version() { - return PreviousSnjsVersion2_0_0 - } - - protected registerStageHandlers() { - this.registerStageHandler(Services.ApplicationStage.PreparingForLaunch_0, async () => { - if (isEnvironmentWebOrDesktop(this.services.environment)) { - await this.migrateStorageStructureForWebDesktop() - } else if (isEnvironmentMobile(this.services.environment)) { - await this.migrateStorageStructureForMobile() - } - }) - this.registerStageHandler(Services.ApplicationStage.StorageDecrypted_09, async () => { - await this.migrateArbitraryRawStorageToManagedStorageAllPlatforms() - if (isEnvironmentMobile(this.services.environment)) { - await this.migrateMobilePreferences() - } - await this.migrateSessionStorage() - await this.deleteLegacyStorageValues() - }) - this.registerStageHandler(Services.ApplicationStage.LoadingDatabase_11, async () => { - await this.createDefaultItemsKeyForAllPlatforms() - this.markDone() - }) - } - - /** - * Web - * Migrates legacy storage structure into new managed format. - * If encrypted storage exists, we need to first decrypt it with the passcode. - * Then extract the account key from it. Then, encrypt storage with the - * account key. Then encrypt the account key with the passcode and store it - * within the new storage format. - * - * Generate note: We do not use the keychain if passcode is available. - */ - private async migrateStorageStructureForWebDesktop() { - const deviceInterface = this.services.deviceInterface - const newStorageRawStructure: Services.StorageValuesObject = { - [Services.ValueModesKeys.Wrapped]: {} as Models.LocalStorageEncryptedContextualPayload, - [Services.ValueModesKeys.Unwrapped]: {}, - [Services.ValueModesKeys.Nonwrapped]: {}, - } - const rawAccountKeyParams = (await this.legacyReader.getAccountKeyParams()) as AnyKeyParamsContent - /** Could be null if no account, or if account and storage is encrypted */ - if (rawAccountKeyParams) { - newStorageRawStructure.nonwrapped[Services.StorageKey.RootKeyParams] = rawAccountKeyParams - } - const encryptedStorage = (await deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.WebEncryptedStorageKey, - )) as Models.EncryptedTransferPayload - - if (encryptedStorage) { - const encryptedStoragePayload = new Models.EncryptedPayload(encryptedStorage) - - const passcodeResult = await this.webDesktopHelperGetPasscodeKeyAndDecryptEncryptedStorage( - encryptedStoragePayload, - ) - - const passcodeKey = passcodeResult.key - const decryptedStoragePayload = passcodeResult.decryptedStoragePayload - const passcodeParams = passcodeResult.keyParams - - newStorageRawStructure.nonwrapped[Services.StorageKey.RootKeyWrapperKeyParams] = passcodeParams.getPortableValue() - - const rawStorageValueStore = Utils.Copy(decryptedStoragePayload.content.storage) - const storageValueStore: Record = Utils.jsonParseEmbeddedKeys(rawStorageValueStore) - /** Store previously encrypted auth_params into new nonwrapped value key */ - - const accountKeyParams = storageValueStore[Services.LegacyKeys1_0_0.AllAccountKeyParamsKey] as AnyKeyParamsContent - newStorageRawStructure.nonwrapped[Services.StorageKey.RootKeyParams] = accountKeyParams - - let keyToEncryptStorageWith = passcodeKey - /** Extract account key (mk, pw, ak) if it exists */ - const hasAccountKeys = !Utils.isNullOrUndefined(storageValueStore.mk) - - if (hasAccountKeys) { - const { accountKey, wrappedKey } = await this.webDesktopHelperExtractAndWrapAccountKeysFromValueStore( - passcodeKey, - accountKeyParams, - storageValueStore, - ) - keyToEncryptStorageWith = accountKey - newStorageRawStructure.nonwrapped[Services.StorageKey.WrappedRootKey] = wrappedKey - } - - /** Encrypt storage with proper key */ - newStorageRawStructure.wrapped = await this.webDesktopHelperEncryptStorage( - keyToEncryptStorageWith, - decryptedStoragePayload, - storageValueStore, - ) - } else { - /** - * No encrypted storage, take account keys (if they exist) out of raw storage - * and place them in the keychain. */ - const ak = await this.services.deviceInterface.getRawStorageValue('ak') - const mk = await this.services.deviceInterface.getRawStorageValue('mk') - - if (ak || mk) { - const version = rawAccountKeyParams.version || (await this.getFallbackRootKeyVersion()) - - const accountKey = CreateNewRootKey({ - masterKey: mk as string, - dataAuthenticationKey: ak as string, - version: version, - keyParams: rawAccountKeyParams, - }) - await this.services.deviceInterface.setNamespacedKeychainValue( - accountKey.getKeychainValue(), - this.services.identifier, - ) - } - } - - /** Persist storage under new key and structure */ - await this.allPlatformHelperSetStorageStructure(newStorageRawStructure) - } - - /** - * Helper - * All platforms - */ - private async allPlatformHelperSetStorageStructure(rawStructure: Services.StorageValuesObject) { - const newStructure = DiskStorageService.DefaultValuesObject( - rawStructure.wrapped, - rawStructure.unwrapped, - rawStructure.nonwrapped, - ) as Partial - - newStructure[Services.ValueModesKeys.Unwrapped] = undefined - - await this.services.deviceInterface.setRawStorageValue( - Services.namespacedKey(this.services.identifier, Services.RawStorageKey.StorageObject), - JSON.stringify(newStructure), - ) - } - - /** - * Helper - * Web/desktop only - */ - private async webDesktopHelperGetPasscodeKeyAndDecryptEncryptedStorage( - encryptedPayload: Models.EncryptedPayloadInterface, - ) { - const rawPasscodeParams = (await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.WebPasscodeParamsKey, - )) as AnyKeyParamsContent - const passcodeParams = this.services.protocolService.createKeyParams(rawPasscodeParams) - - /** Decrypt it with the passcode */ - let decryptedStoragePayload: - | Models.DecryptedPayloadInterface - | Models.EncryptedPayloadInterface = encryptedPayload - let passcodeKey: SNRootKey | undefined - - await this.promptForPasscodeUntilCorrect(async (candidate: string) => { - passcodeKey = await this.services.protocolService.computeRootKey(candidate, passcodeParams) - decryptedStoragePayload = await this.services.protocolService.decryptSplitSingle({ - usesRootKey: { - items: [encryptedPayload], - key: passcodeKey, - }, - }) - - return !Models.isErrorDecryptingPayload(decryptedStoragePayload) - }) - - return { - decryptedStoragePayload: - decryptedStoragePayload as unknown as Models.DecryptedPayloadInterface, - key: passcodeKey as SNRootKey, - keyParams: passcodeParams, - } - } - - /** - * Helper - * Web/desktop only - */ - private async webDesktopHelperExtractAndWrapAccountKeysFromValueStore( - passcodeKey: SNRootKey, - accountKeyParams: AnyKeyParamsContent, - storageValueStore: Record, - ) { - const version = accountKeyParams?.version || (await this.getFallbackRootKeyVersion()) - const accountKey = CreateNewRootKey({ - masterKey: storageValueStore.mk as string, - dataAuthenticationKey: storageValueStore.ak as string, - version: version, - keyParams: accountKeyParams, - }) - - delete storageValueStore.mk - delete storageValueStore.pw - delete storageValueStore.ak - - const accountKeyPayload = accountKey.payload - - /** Encrypt account key with passcode */ - const encryptedAccountKey = await this.services.protocolService.encryptSplitSingle({ - usesRootKey: { - items: [accountKeyPayload], - key: passcodeKey, - }, - }) - return { - accountKey: accountKey, - wrappedKey: Models.CreateEncryptedLocalStorageContextPayload(encryptedAccountKey), - } - } - - /** - * Helper - * Web/desktop only - * Encrypt storage with account key - */ - async webDesktopHelperEncryptStorage( - key: SNRootKey, - decryptedStoragePayload: Models.DecryptedPayloadInterface, - storageValueStore: Record, - ) { - const wrapped = await this.services.protocolService.encryptSplitSingle({ - usesRootKey: { - items: [ - decryptedStoragePayload.copy({ - content_type: ContentType.EncryptedStorage, - content: storageValueStore as unknown as Models.ItemContent, - }), - ], - key: key, - }, - }) - - return Models.CreateEncryptedLocalStorageContextPayload(wrapped) - } - - /** - * Mobile - * On mobile legacy structure is mostly similar to new structure, - * in that the account key is encrypted with the passcode. But mobile did - * not have encrypted storage, so we simply need to transfer all existing - * storage values into new managed structure. - * - * In version <= 3.0.16 on mobile, encrypted account keys were stored in the keychain - * under `encryptedAccountKeys`. In 3.0.17 a migration was introduced that moved this value - * to storage under key `encrypted_account_keys`. We need to anticipate the keys being in - * either location. - * - * If no account but passcode only, the only thing we stored on mobile - * previously was keys.offline.pw and keys.offline.timing in the keychain - * that we compared against for valid decryption. - * In the new version, we know a passcode is correct if it can decrypt storage. - * As part of the migration, we’ll need to request the raw passcode from user, - * compare it against the keychain offline.pw value, and if correct, - * migrate storage to new structure, and encrypt with passcode key. - * - * If account only, take the value in the keychain, and rename the values - * (i.e mk > masterKey). - * @access private - */ - async migrateStorageStructureForMobile() { - Utils.assert(isMobileDevice(this.services.deviceInterface)) - - const keychainValue = - (await this.services.deviceInterface.getRawKeychainValue()) as unknown as LegacyMobileKeychainStructure - - const wrappedAccountKey = ((await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobileWrappedRootKeyKey, - )) || keychainValue?.encryptedAccountKeys) as Models.EncryptedTransferPayload - - const rawAccountKeyParams = (await this.legacyReader.getAccountKeyParams()) as AnyKeyParamsContent - - const rawPasscodeParams = (await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobilePasscodeParamsKey, - )) as AnyKeyParamsContent - - const firstRunValue = await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.NonwrappedStorageKey.MobileFirstRun, - ) - - const rawStructure: Services.StorageValuesObject = { - [Services.ValueModesKeys.Nonwrapped]: { - [Services.StorageKey.WrappedRootKey]: wrappedAccountKey, - /** A 'hash' key may be present from legacy versions that should be deleted */ - [Services.StorageKey.RootKeyWrapperKeyParams]: Utils.omitByCopy(rawPasscodeParams, ['hash' as never]), - [Services.StorageKey.RootKeyParams]: rawAccountKeyParams, - [Services.NonwrappedStorageKey.MobileFirstRun]: firstRunValue, - }, - [Services.ValueModesKeys.Unwrapped]: {}, - [Services.ValueModesKeys.Wrapped]: {} as Models.LocalStorageDecryptedContextualPayload, - } - - const biometricPrefs = (await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobileBiometricsPrefs, - )) as { enabled: boolean; timing: unknown } - - if (biometricPrefs) { - rawStructure.nonwrapped[Services.StorageKey.BiometricsState] = biometricPrefs.enabled - rawStructure.nonwrapped[Services.StorageKey.MobileBiometricsTiming] = biometricPrefs.timing - } - - const passcodeKeyboardType = await this.services.deviceInterface.getRawStorageValue( - Services.LegacyKeys1_0_0.MobilePasscodeKeyboardType, - ) - - if (passcodeKeyboardType) { - rawStructure.nonwrapped[Services.StorageKey.MobilePasscodeKeyboardType] = passcodeKeyboardType - } - - if (rawPasscodeParams) { - const passcodeParams = this.services.protocolService.createKeyParams(rawPasscodeParams) - const getPasscodeKey = async () => { - let passcodeKey: SNRootKey | undefined - - await this.promptForPasscodeUntilCorrect(async (candidate: string) => { - passcodeKey = await this.services.protocolService.computeRootKey(candidate, passcodeParams) - - const pwHash = keychainValue?.offline?.pw - - if (pwHash) { - return passcodeKey.serverPassword === pwHash - } else { - /** - * Fallback decryption if keychain is missing for some reason. If account, - * validate by attempting to decrypt wrapped account key. Otherwise, validate - * by attempting to decrypt random item. - * */ - if (wrappedAccountKey) { - const decryptedAcctKey = await this.services.protocolService.decryptSplitSingle({ - usesRootKey: { - items: [new Models.EncryptedPayload(wrappedAccountKey)], - key: passcodeKey, - }, - }) - return !Models.isErrorDecryptingPayload(decryptedAcctKey) - } else { - const item = ( - await this.services.deviceInterface.getAllRawDatabasePayloads(this.services.identifier) - )[0] as Models.EncryptedTransferPayload - - if (!item) { - throw Error('Passcode only migration aborting due to missing keychain.offline.pw') - } - - const decryptedPayload = await this.services.protocolService.decryptSplitSingle({ - usesRootKey: { - items: [new Models.EncryptedPayload(item)], - key: passcodeKey, - }, - }) - return !Models.isErrorDecryptingPayload(decryptedPayload) - } - } - }) - - return passcodeKey as SNRootKey - } - - rawStructure.nonwrapped[Services.StorageKey.MobilePasscodeTiming] = keychainValue?.offline?.timing - - if (wrappedAccountKey) { - /** - * Account key is encrypted with passcode. Inside, the accountKey is located inside - * content.accountKeys. We want to unembed these values to main content, rename - * with proper property names, wrap again, and store in new rawStructure. - */ - const passcodeKey = await getPasscodeKey() - const payload = new Models.EncryptedPayload(wrappedAccountKey) - const unwrappedAccountKey = await this.services.protocolService.decryptSplitSingle({ - usesRootKey: { - items: [payload], - key: passcodeKey, - }, - }) - - if (Models.isErrorDecryptingPayload(unwrappedAccountKey)) { - return - } - - const accountKeyContent = unwrappedAccountKey.content.accountKeys as LegacyAccountKeysValue - - const version = - accountKeyContent.version || rawAccountKeyParams?.version || (await this.getFallbackRootKeyVersion()) - - const newAccountKey = unwrappedAccountKey.copy({ - content: Models.FillItemContent({ - masterKey: accountKeyContent.mk, - dataAuthenticationKey: accountKeyContent.ak, - version: version as ProtocolVersion, - keyParams: rawAccountKeyParams, - accountKeys: undefined, - }), - }) - - const newWrappedAccountKey = await this.services.protocolService.encryptSplitSingle({ - usesRootKey: { - items: [newAccountKey], - key: passcodeKey, - }, - }) - rawStructure.nonwrapped[Services.StorageKey.WrappedRootKey] = - Models.CreateEncryptedLocalStorageContextPayload(newWrappedAccountKey) - - if (accountKeyContent.jwt) { - /** Move the jwt to raw storage so that it can be migrated in `migrateSessionStorage` */ - void this.services.deviceInterface.setRawStorageValue(LEGACY_SESSION_TOKEN_KEY, accountKeyContent.jwt) - } - await this.services.deviceInterface.clearRawKeychainValue() - } else if (!wrappedAccountKey) { - /** Passcode only, no account */ - const passcodeKey = await getPasscodeKey() - const payload = new Models.DecryptedPayload({ - uuid: Utils.UuidGenerator.GenerateUuid(), - content: Models.FillItemContent(rawStructure.unwrapped), - content_type: ContentType.EncryptedStorage, - ...PayloadTimestampDefaults(), - }) - - /** Encrypt new storage.unwrapped structure with passcode */ - const wrapped = await this.services.protocolService.encryptSplitSingle({ - usesRootKey: { - items: [payload], - key: passcodeKey, - }, - }) - rawStructure.wrapped = Models.CreateEncryptedLocalStorageContextPayload(wrapped) - - await this.services.deviceInterface.clearRawKeychainValue() - } - } else { - /** No passcode, potentially account. Migrate keychain property keys. */ - const hasAccount = !Utils.isNullOrUndefined(keychainValue?.mk) - if (hasAccount) { - const accountVersion = - (keychainValue.version as ProtocolVersion) || - rawAccountKeyParams?.version || - (await this.getFallbackRootKeyVersion()) - - const accountKey = CreateNewRootKey({ - masterKey: keychainValue.mk, - dataAuthenticationKey: keychainValue.ak, - version: accountVersion, - keyParams: rawAccountKeyParams, - }) - - await this.services.deviceInterface.setNamespacedKeychainValue( - accountKey.getKeychainValue(), - this.services.identifier, - ) - - if (keychainValue.jwt) { - /** Move the jwt to raw storage so that it can be migrated in `migrateSessionStorage` */ - void this.services.deviceInterface.setRawStorageValue(LEGACY_SESSION_TOKEN_KEY, keychainValue.jwt) - } - } - } - - /** Move encrypted account key into place where it is now expected */ - await this.allPlatformHelperSetStorageStructure(rawStructure) - } - - /** - * If we are unable to determine a root key's version, due to missing version - * parameter from key params due to 001 or 002, we need to fallback to checking - * any encrypted payload and retrieving its version. - * - * If we are unable to garner any meaningful information, we will default to 002. - * - * (Previously we attempted to discern version based on presence of keys.ak; if ak, - * then 003, otherwise 002. However, late versions of 002 also inluded an ak, so this - * method can't be used. This method also didn't account for 001 versions.) - */ - private async getFallbackRootKeyVersion() { - const anyItem = ( - await this.services.deviceInterface.getAllRawDatabasePayloads(this.services.identifier) - )[0] as Models.EncryptedTransferPayload - - if (!anyItem) { - return ProtocolVersion.V002 - } - - const payload = new Models.EncryptedPayload(anyItem) - return payload.version || ProtocolVersion.V002 - } - - /** - * All platforms - * Migrate all previously independently stored storage keys into new - * managed approach. - */ - private async migrateArbitraryRawStorageToManagedStorageAllPlatforms() { - const allKeyValues = await this.services.deviceInterface.getAllRawStorageKeyValues() - const legacyKeys = Utils.objectToValueArray(Services.LegacyKeys1_0_0) - - const tryJsonParse = (value: string) => { - try { - return JSON.parse(value) - } catch (e) { - return value - } - } - - const applicationIdentifier = this.services.identifier - - for (const keyValuePair of allKeyValues) { - const key = keyValuePair.key - const value = keyValuePair.value - const isNameSpacedKey = - applicationIdentifier && applicationIdentifier.length > 0 && key.startsWith(applicationIdentifier) - if (legacyKeys.includes(key) || isNameSpacedKey) { - continue - } - if (!Utils.isNullOrUndefined(value)) { - /** - * Raw values should always have been json stringified. - * New values should always be objects/parsed. - */ - const newValue = tryJsonParse(value as string) - this.services.storageService.setValue(key, newValue) - } - } - } - - /** - * All platforms - * Deletes all StorageKey and LegacyKeys1_0_0 from root raw storage. - * @access private - */ - async deleteLegacyStorageValues() { - const miscKeys = [ - 'mk', - 'ak', - 'pw', - /** v1 unused key */ - 'encryptionKey', - /** v1 unused key */ - 'authKey', - 'jwt', - 'ephemeral', - 'cachedThemes', - ] - - const managedKeys = [ - ...Utils.objectToValueArray(Services.StorageKey), - ...Utils.objectToValueArray(Services.LegacyKeys1_0_0), - ...miscKeys, - ] - - for (const key of managedKeys) { - await this.services.deviceInterface.removeRawStorageValue(key) - } - } - - /** - * Mobile - * Migrate mobile preferences - */ - private async migrateMobilePreferences() { - const lastExportDate = await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobileLastExportDate, - ) - const doNotWarnUnsupportedEditors = await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobileDoNotWarnUnsupportedEditors, - ) - const legacyOptionsState = (await this.services.deviceInterface.getJsonParsedRawStorageValue( - Services.LegacyKeys1_0_0.MobileOptionsState, - )) as Record - - let migratedOptionsState = {} - - if (legacyOptionsState) { - const legacySortBy = legacyOptionsState.sortBy - migratedOptionsState = { - sortBy: - legacySortBy === 'updated_at' || legacySortBy === 'client_updated_at' - ? Models.CollectionSort.UpdatedAt - : legacySortBy, - sortReverse: legacyOptionsState.sortReverse ?? false, - hideNotePreview: legacyOptionsState.hidePreviews ?? false, - hideDate: legacyOptionsState.hideDates ?? false, - hideTags: legacyOptionsState.hideTags ?? false, - } - } - const preferences = { - ...migratedOptionsState, - lastExportDate: lastExportDate ?? undefined, - doNotShowAgainUnsupportedEditors: doNotWarnUnsupportedEditors ?? false, - } - await this.services.storageService.setValue(Services.StorageKey.MobilePreferences, preferences) - } - - /** - * All platforms - * Migrate previously stored session string token into object - * On mobile, JWTs were previously stored in storage, inside of the user object, - * but then custom-migrated to be stored in the keychain. We must account for - * both scenarios here in case a user did not perform the custom platform migration. - * On desktop/web, JWT was stored in storage. - */ - private migrateSessionStorage() { - const USER_OBJECT_KEY = 'user' - let currentToken = this.services.storageService.getValue(LEGACY_SESSION_TOKEN_KEY) - const user = this.services.storageService.getValue<{ jwt: string; server: string }>(USER_OBJECT_KEY) - - if (!currentToken) { - /** Try the user object */ - if (user) { - currentToken = user.jwt - } - } - - if (!currentToken) { - /** - * If we detect that a user object is present, but the jwt is missing, - * we'll fill the jwt value with a junk value just so we create a session. - * When the client attempts to talk to the server, the server will reply - * with invalid token error, and the client will automatically prompt to reauthenticate. - */ - const hasAccount = !Utils.isNullOrUndefined(user) - if (hasAccount) { - currentToken = 'junk-value' - } else { - return - } - } - - const sessionOrError = LegacySession.create(currentToken) - if (!sessionOrError.isFailed()) { - this.services.storageService.setValue( - Services.StorageKey.Session, - this.services.legacySessionStorageMapper.toProjection(sessionOrError.getValue()), - ) - } - - /** Server has to be migrated separately on mobile */ - if (isEnvironmentMobile(this.services.environment)) { - if (user && user.server) { - this.services.storageService.setValue(Services.StorageKey.ServerHost, user.server) - } - } - } - - /** - * All platforms - * Create new default items key from root key. - * Otherwise, when data is loaded, we won't be able to decrypt it - * without existence of an item key. This will mean that if this migration - * is run on two different platforms for the same user, they will create - * two new items keys. Which one they use to decrypt past items and encrypt - * future items doesn't really matter. - * @access private - */ - async createDefaultItemsKeyForAllPlatforms() { - const rootKey = this.services.protocolService.getRootKey() - if (rootKey) { - const rootKeyParams = await this.services.protocolService.getRootKeyParams() - /** If params are missing a version, it must be 001 */ - const fallbackVersion = ProtocolVersion.V001 - - const payload = new Models.DecryptedPayload({ - uuid: Utils.UuidGenerator.GenerateUuid(), - content_type: ContentType.ItemsKey, - content: Models.FillItemContentSpecialized({ - itemsKey: rootKey.masterKey, - dataAuthenticationKey: rootKey.dataAuthenticationKey, - version: rootKeyParams?.version || fallbackVersion, - }), - dirty: true, - dirtyIndex: getIncrementedDirtyIndex(), - ...PayloadTimestampDefaults(), - }) - - const itemsKey = Models.CreateDecryptedItemFromPayload(payload) - - await this.services.itemManager.emitItemFromPayload( - itemsKey.payloadRepresentation(), - Models.PayloadEmitSource.LocalChanged, - ) - } - } -} diff --git a/packages/snjs/lib/Migrations/Versions/index.ts b/packages/snjs/lib/Migrations/Versions/index.ts index b065864e9..15f5de331 100644 --- a/packages/snjs/lib/Migrations/Versions/index.ts +++ b/packages/snjs/lib/Migrations/Versions/index.ts @@ -1,17 +1,9 @@ -import { Migration2_0_0 } from './2_0_0' import { Migration2_0_15 } from './2_0_15' import { Migration2_7_0 } from './2_7_0' import { Migration2_20_0 } from './2_20_0' import { Migration2_36_0 } from './2_36_0' import { Migration2_42_0 } from './2_42_0' -export const MigrationClasses = [ - Migration2_0_0, - Migration2_0_15, - Migration2_7_0, - Migration2_20_0, - Migration2_36_0, - Migration2_42_0, -] +export const MigrationClasses = [Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0] -export { Migration2_0_0, Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0 } +export { Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0 } diff --git a/packages/snjs/lib/Services/Preferences/PreferencesService.ts b/packages/snjs/lib/Services/Preferences/PreferencesService.ts index 3be576966..49a7f1166 100644 --- a/packages/snjs/lib/Services/Preferences/PreferencesService.ts +++ b/packages/snjs/lib/Services/Preferences/PreferencesService.ts @@ -83,7 +83,7 @@ export class SNPreferencesService void this.notifyEvent(PreferencesServiceEvent.PreferencesChanged) - void this.syncService.sync() + void this.syncService.sync({ sourceDescription: 'PreferencesService.setValue' }) } private async reload() { diff --git a/packages/snjs/lib/Services/Singleton/SingletonManager.ts b/packages/snjs/lib/Services/Singleton/SingletonManager.ts index 4cf8f0702..c37f91c91 100644 --- a/packages/snjs/lib/Services/Singleton/SingletonManager.ts +++ b/packages/snjs/lib/Services/Singleton/SingletonManager.ts @@ -133,7 +133,7 @@ export class SNSingletonManager extends AbstractService { * of a download-first request. */ if (handled.length > 0 && eventSource === SyncEvent.SyncCompletedWithAllItemsUploaded) { - await this.syncService?.sync() + await this.syncService?.sync({ sourceDescription: 'Resolve singletons for items' }) } } @@ -190,7 +190,7 @@ export class SNSingletonManager extends AbstractService { } }) - await this.syncService.sync() + await this.syncService.sync({ sourceDescription: 'Find or create singleton, before any sync has completed' }) removeObserver() @@ -224,7 +224,7 @@ export class SNSingletonManager extends AbstractService { const item = await this.itemManager.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted) - void this.syncService.sync() + void this.syncService.sync({ sourceDescription: 'After find or create singleton' }) return item as T } diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index dde5e411c..4a01cb0d9 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -20,6 +20,7 @@ import { PayloadTimestampDefaults, LocalStorageEncryptedContextualPayload, Environment, + FullyFormedTransferPayload, } from '@standardnotes/models' /** @@ -377,8 +378,8 @@ export class DiskStorageService extends Services.AbstractService implements Serv await this.immediatelyPersistValuesToDisk() } - public async getAllRawPayloads() { - return this.deviceInterface.getAllRawDatabasePayloads(this.identifier) + public async getAllRawPayloads(): Promise { + return this.deviceInterface.getAllDatabaseEntries(this.identifier) } public async savePayload(payload: FullyFormedPayloadInterface): Promise { @@ -432,7 +433,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv const exportedDeleted = deleted.map(CreateDeletedLocalStorageContextPayload) return this.executeCriticalFunction(async () => { - return this.deviceInterface?.saveRawDatabasePayloads( + return this.deviceInterface?.saveDatabaseEntries( [...exportedEncrypted, ...exportedDecrypted, ...exportedDeleted], this.identifier, ) @@ -449,13 +450,13 @@ export class DiskStorageService extends Services.AbstractService implements Serv public async deletePayloadWithId(uuid: Uuid) { return this.executeCriticalFunction(async () => { - return this.deviceInterface.removeRawDatabasePayloadWithId(uuid, this.identifier) + return this.deviceInterface.removeDatabaseEntry(uuid, this.identifier) }) } public async clearAllPayloads() { return this.executeCriticalFunction(async () => { - return this.deviceInterface.removeAllRawDatabasePayloads(this.identifier) + return this.deviceInterface.removeAllDatabaseEntries(this.identifier) }) } @@ -482,7 +483,6 @@ export class DiskStorageService extends Services.AbstractService implements Serv currentPersistPromise: this.currentPersistPromise != undefined, isStorageWrapped: this.isStorageWrapped(), allRawPayloadsCount: (await this.getAllRawPayloads()).length, - databaseKeys: await this.deviceInterface.getDatabaseKeys(), }, } } diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index cdfb42c81..60e45f782 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -1,3 +1,4 @@ +import { log, LoggingDomain } from './../../Logging' import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation' import { ContentType } from '@standardnotes/common' import { @@ -18,7 +19,6 @@ import { SNHistoryManager } from '../History/HistoryManager' import { SNLog } from '@Lib/Log' import { SNSessionManager } from '../Session/SessionManager' import { DiskStorageService } from '../Storage/DiskStorageService' -import { GetSortedPayloadsByPriority } from '@Lib/Services/Sync/Utils' import { SyncClientInterface } from './SyncClientInterface' import { SyncPromise } from './Types' import { SyncOpStatus } from '@Lib/Services/Sync/SyncOpStatus' @@ -33,7 +33,6 @@ import { DeltaOutOfSync, ImmutablePayloadCollection, CreatePayload, - FullyFormedTransferPayload, isEncryptedPayload, isDecryptedPayload, EncryptedPayloadInterface, @@ -74,6 +73,9 @@ import { SyncServiceInterface, DiagnosticInfo, EncryptionService, + DeviceInterface, + isFullEntryLoadChunkResponse, + isChunkFullEntry, } from '@standardnotes/services' import { OfflineSyncResponse } from './Offline/Response' import { @@ -142,6 +144,8 @@ export class SNSyncService private payloadManager: PayloadManager, private apiService: SNApiService, private historyService: SNHistoryManager, + private device: DeviceInterface, + private identifier: string, private readonly options: ApplicationSyncOptions, protected override internalEventBus: InternalEventBusInterface, ) { @@ -221,19 +225,13 @@ export class SNSyncService return this.databaseLoaded } - /** - * Used in tandem with `loadDatabasePayloads` - */ - public async getDatabasePayloads(): Promise { - return this.storageService.getAllRawPayloads().catch((error) => { - void this.notifyEvent(SyncEvent.DatabaseReadError, error) - throw error - }) - } - private async processItemsKeysFirstDuringDatabaseLoad( itemsKeysPayloads: FullyFormedPayloadInterface[], ): Promise { + if (itemsKeysPayloads.length === 0) { + return + } + const encryptedItemsKeysPayloads = itemsKeysPayloads.filter(isEncryptedPayload) const originallyDecryptedItemsKeysPayloads = itemsKeysPayloads.filter( @@ -254,57 +252,69 @@ export class SNSyncService ) } - /** - * @param rawPayloads - use `getDatabasePayloads` to get these payloads. - * They are fed as a parameter so that callers don't have to await the loading, but can - * await getting the raw payloads from storage - */ - public async loadDatabasePayloads(rawPayloads: FullyFormedTransferPayload[]): Promise { + public async loadDatabasePayloads(): Promise { + log(LoggingDomain.DatabaseLoad, 'Loading database payloads') + if (this.databaseLoaded) { throw 'Attempting to initialize already initialized local database.' } - if (rawPayloads.length === 0) { - this.databaseLoaded = true - this.opStatus.setDatabaseLoadStatus(0, 0, true) - return - } + const chunks = await this.device.getDatabaseLoadChunks( + { + batchSize: this.options.loadBatchSize, + contentTypePriority: this.localLoadPriorty, + uuidPriority: this.launchPriorityUuids, + }, + this.identifier, + ) - const unsortedPayloads = rawPayloads - .map((rawPayload) => { + const itemsKeyEntries = isFullEntryLoadChunkResponse(chunks) + ? chunks.fullEntries.itemsKeys.entries + : await this.device.getDatabaseEntries(this.identifier, chunks.keys.itemsKeys.keys) + + const itemsKeyPayloads = itemsKeyEntries + .map((entry) => { try { - return CreatePayload(rawPayload, PayloadSource.Constructor) + return CreatePayload(entry, PayloadSource.Constructor) } catch (e) { - console.error('Creating payload fail+ed', e) + console.error('Creating payload failed', e) return undefined } }) .filter(isNotUndefined) - const { itemsKeyPayloads, contentTypePriorityPayloads, remainingPayloads } = GetSortedPayloadsByPriority( - unsortedPayloads, - this.localLoadPriorty, - this.launchPriorityUuids, - ) - await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeyPayloads) - await this.processPayloadBatch(contentTypePriorityPayloads) - /** * Map in batches to give interface a chance to update. Note that total decryption * time is constant regardless of batch size. Decrypting 3000 items all at once or in * batches will result in the same time spent. It's the emitting/painting/rendering * that requires batch size optimization. */ - const payloadCount = remainingPayloads.length - const batchSize = this.options.loadBatchSize - const numBatches = Math.ceil(payloadCount / batchSize) + const payloadCount = chunks.remainingChunksItemCount + let totalProcessedCount = 0 - for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { - const currentPosition = batchIndex * batchSize - const batch = remainingPayloads.slice(currentPosition, currentPosition + batchSize) - await this.processPayloadBatch(batch, currentPosition, payloadCount) + const remainingChunks = isFullEntryLoadChunkResponse(chunks) + ? chunks.fullEntries.remainingChunks + : chunks.keys.remainingChunks + + for (const chunk of remainingChunks) { + const dbEntries = isChunkFullEntry(chunk) + ? chunk.entries + : await this.device.getDatabaseEntries(this.identifier, chunk.keys) + const payloads = dbEntries + .map((entry) => { + try { + return CreatePayload(entry, PayloadSource.Constructor) + } catch (e) { + console.error('Creating payload failed', e) + return undefined + } + }) + .filter(isNotUndefined) + + await this.processPayloadBatch(payloads, totalProcessedCount, payloadCount) + totalProcessedCount += payloads.length } this.databaseLoaded = true @@ -316,6 +326,7 @@ export class SNSyncService currentPosition?: number, payloadCount?: number, ) { + log(LoggingDomain.DatabaseLoad, 'Processing batch at index', currentPosition, 'length', batch.length) const encrypted: EncryptedPayloadInterface[] = [] const nonencrypted: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] @@ -386,7 +397,7 @@ export class SNSyncService } public async markAllItemsAsNeedingSyncAndPersist(): Promise { - this.log('Marking all items as needing sync') + log(LoggingDomain.Sync, 'Marking all items as needing sync') const items = this.itemManager.items const payloads = items.map((item) => { @@ -444,7 +455,7 @@ export class SNSyncService const promise = this.spawnQueue[0] removeFromIndex(this.spawnQueue, 0) - this.log('Syncing again from spawn queue') + log(LoggingDomain.Sync, 'Syncing again from spawn queue') return this.sync({ queueStrategy: SyncQueueStrategy.ForceSpawnNew, @@ -506,7 +517,7 @@ export class SNSyncService public async sync(options: Partial = {}): Promise { if (this.clientLocked) { - this.log('Sync locked by client') + log(LoggingDomain.Sync, 'Sync locked by client') return } @@ -562,7 +573,7 @@ export class SNSyncService * (before reaching opStatus.setDidBegin). * 2. syncOpInProgress: If a sync() call is in flight to the server. */ - private configureSyncLock() { + private configureSyncLock(options: SyncOptions) { const syncInProgress = this.opStatus.syncInProgress const databaseLoaded = this.databaseLoaded const canExecuteSync = !this.syncLock @@ -571,12 +582,14 @@ export class SNSyncService if (shouldExecuteSync) { this.syncLock = true } else { - this.log( + log( + LoggingDomain.Sync, !canExecuteSync ? 'Another function call has begun preparing for sync.' : syncInProgress ? 'Attempting to sync while existing sync in progress.' : 'Attempting to sync before local database has loaded.', + options, ) } @@ -656,10 +669,20 @@ export class SNSyncService private createOfflineSyncOperation( payloads: (DeletedPayloadInterface | DecryptedPayloadInterface)[], - source: SyncSource, - mode: SyncMode = SyncMode.Default, + options: SyncOptions, ) { - this.log('Syncing offline user', 'source:', source, 'mode:', mode, 'payloads:', payloads) + log( + LoggingDomain.Sync, + 'Syncing offline user', + 'source:', + SyncSource[options.source], + 'sourceDesc', + options.sourceDescription, + 'mode:', + options.mode && SyncMode[options.mode], + 'payloads:', + payloads, + ) const operation = new OfflineSyncOperation(payloads, async (type, response) => { if (this.dealloced) { @@ -727,7 +750,8 @@ export class SNSyncService this.apiService, ) - this.log( + log( + LoggingDomain.Sync, 'Syncing online user', 'source', SyncSource[source], @@ -769,14 +793,14 @@ export class SNSyncService const { uploadPayloads } = this.getOfflineSyncParameters(payloads, options.mode) return { - operation: this.createOfflineSyncOperation(uploadPayloads, options.source, options.mode), + operation: this.createOfflineSyncOperation(uploadPayloads, options), mode: options.mode || SyncMode.Default, } } } private async performSync(options: SyncOptions): Promise { - const { shouldExecuteSync, releaseLock } = this.configureSyncLock() + const { shouldExecuteSync, releaseLock } = this.configureSyncLock(options) const { items, beginDate, frozenDirtyIndex, neverSyncedDeleted } = await this.prepareForSync(options) @@ -843,7 +867,7 @@ export class SNSyncService } private async handleOfflineResponse(response: OfflineSyncResponse) { - this.log('Offline Sync Response', response) + log(LoggingDomain.Sync, 'Offline Sync Response', response) const masterCollection = this.payloadManager.getMasterCollection() @@ -861,7 +885,7 @@ export class SNSyncService } private handleErrorServerResponse(response: ServerSyncResponse) { - this.log('Sync Error', response) + log(LoggingDomain.Sync, 'Sync Error', response) if (response.status === INVALID_SESSION_RESPONSE_STATUS) { void this.notifyEvent(SyncEvent.InvalidSession) @@ -904,7 +928,8 @@ export class SNSyncService historyMap, ) - this.log( + log( + LoggingDomain.Sync, 'Online Sync Response', 'Operator ID', operation.id, @@ -1060,7 +1085,7 @@ export class SNSyncService } private async syncAgainByHandlingRequestsWaitingInResolveQueue(options: SyncOptions) { - this.log('Syncing again from resolve queue') + log(LoggingDomain.Sync, 'Syncing again from resolve queue') const promise = this.sync({ source: SyncSource.ResolveQueue, checkIntegrity: options.checkIntegrity, diff --git a/packages/snjs/lib/Services/Sync/index.ts b/packages/snjs/lib/Services/Sync/index.ts index 32acb4930..b60c1a013 100644 --- a/packages/snjs/lib/Services/Sync/index.ts +++ b/packages/snjs/lib/Services/Sync/index.ts @@ -5,5 +5,4 @@ export * from './SyncClientInterface' export * from './Account/Operation' export * from './Account/ResponseResolver' export * from './Offline/Operation' -export * from './Utils' export * from './Account/Response' diff --git a/packages/snjs/mocha/application.test.js b/packages/snjs/mocha/application.test.js index e05569e92..779a26e97 100644 --- a/packages/snjs/mocha/application.test.js +++ b/packages/snjs/mocha/application.test.js @@ -110,12 +110,12 @@ describe('application instances', () => { * app deinit. */ await Factory.sleep(MaximumWaitTime - 0.05) /** Access any deviceInterface function */ - app.diskStorageService.deviceInterface.getAllRawDatabasePayloads(app.identifier) + app.diskStorageService.deviceInterface.getAllDatabaseEntries(app.identifier) }) await app.lock() }) - describe('signOut()', () => { + describe.skip('signOut()', () => { let testNote1 let confirmAlert let deinit diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index d91e4fcbf..8fe960457 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -59,9 +59,6 @@ describe('basic auth', function () { expect(await this.application.protocolService.getRootKey()).to.not.be.ok expect(this.application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyNone) - - const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() - expect(rawPayloads.length).to.equal(BaseItemCounts.DefaultItems) }) it('successfully signs in to registered account', async function () { diff --git a/packages/snjs/mocha/key_recovery_service.test.js b/packages/snjs/mocha/key_recovery_service.test.js index 957cd4cbb..74a426d25 100644 --- a/packages/snjs/mocha/key_recovery_service.test.js +++ b/packages/snjs/mocha/key_recovery_service.test.js @@ -664,12 +664,12 @@ describe('key recovery service', function () { await Factory.awaitFunctionInvokation(appA.keyRecoveryService, 'handleDecryptionOfAllKeysMatchingCorrectRootKey') /** Stored version of items key should use new root key */ - const stored = (await appA.deviceInterface.getAllRawDatabasePayloads(appA.identifier)).find( + const stored = (await appA.deviceInterface.getAllDatabaseEntries(appA.identifier)).find( (payload) => payload.uuid === newDefaultKey.uuid, ) const storedParams = await appA.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(stored)) - const correctStored = (await appB.deviceInterface.getAllRawDatabasePayloads(appB.identifier)).find( + const correctStored = (await appB.deviceInterface.getAllDatabaseEntries(appB.identifier)).find( (payload) => payload.uuid === newDefaultKey.uuid, ) diff --git a/packages/snjs/mocha/lib/factory.js b/packages/snjs/mocha/lib/factory.js index 80b222f6a..d2cc61e82 100644 --- a/packages/snjs/mocha/lib/factory.js +++ b/packages/snjs/mocha/lib/factory.js @@ -303,7 +303,8 @@ export function tomorrow() { return new Date(new Date().setDate(new Date().getDate() + 1)) } -export async function sleep(seconds) { +export async function sleep(seconds, reason) { + console.log('Sleeping for reason', reason) return Utils.sleep(seconds) } diff --git a/packages/snjs/mocha/lib/web_device_interface.js b/packages/snjs/mocha/lib/web_device_interface.js index 9b7e93c2b..78daa9a25 100644 --- a/packages/snjs/mocha/lib/web_device_interface.js +++ b/packages/snjs/mocha/lib/web_device_interface.js @@ -21,17 +21,6 @@ export default class WebDeviceInterface { } } - async getAllRawStorageKeyValues() { - const results = [] - for (const key of Object.keys(localStorage)) { - results.push({ - key: key, - value: localStorage[key], - }) - } - return results - } - async setRawStorageValue(key, value) { localStorage.setItem(key, value) } @@ -60,7 +49,7 @@ export default class WebDeviceInterface { return `${this._getDatabaseKeyPrefix(identifier)}${id}` } - async getAllRawDatabasePayloads(identifier) { + async getAllDatabaseEntries(identifier) { const models = [] for (const key in localStorage) { if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) { @@ -70,21 +59,51 @@ export default class WebDeviceInterface { return models } - async saveRawDatabasePayload(payload, identifier) { + async getDatabaseLoadChunks(options, identifier) { + const entries = await this.getAllDatabaseEntries(identifier) + const sorted = GetSortedPayloadsByPriority(entries, options) + + const itemsKeysChunk = { + entries: sorted.itemsKeyPayloads, + } + + const contentTypePriorityChunk = { + entries: sorted.contentTypePriorityPayloads, + } + + const remainingPayloadsChunks = [] + for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) { + remainingPayloadsChunks.push({ + entries: sorted.remainingPayloads.slice(i, i + options.batchSize), + }) + } + + const result = { + fullEntries: { + itemsKeys: itemsKeysChunk, + remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks], + }, + remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + } + + return result + } + + async saveDatabaseEntry(payload, identifier) { localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload)) } - async saveRawDatabasePayloads(payloads, identifier) { + async saveDatabaseEntries(payloads, identifier) { for (const payload of payloads) { - await this.saveRawDatabasePayload(payload, identifier) + await this.saveDatabaseEntry(payload, identifier) } } - async removeRawDatabasePayloadWithId(id, identifier) { + async removeDatabaseEntry(id, identifier) { localStorage.removeItem(this._keyForPayloadId(id, identifier)) } - async removeAllRawDatabasePayloads(identifier) { + async removeAllDatabaseEntries(identifier) { for (const key in localStorage) { if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) { delete localStorage[key] @@ -124,12 +143,6 @@ export default class WebDeviceInterface { localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain)) } - /** Allows unit tests to set legacy keychain structure as it was <= 003 */ - // eslint-disable-next-line camelcase - async setLegacyRawKeychainValue(value) { - localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value)) - } - async getRawKeychainValue() { const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY) return JSON.parse(keychain) @@ -139,19 +152,13 @@ export default class WebDeviceInterface { localStorage.removeItem(KEYCHAIN_STORAGE_KEY) } - performSoftReset() { + performSoftReset() {} - } - - performHardReset() { - - } + performHardReset() {} isDeviceDestroyed() { return false } - deinit() { - - } + deinit() {} } diff --git a/packages/snjs/mocha/migrations/2020-01-15-mobile.test.js b/packages/snjs/mocha/migrations/2020-01-15-mobile.test.js deleted file mode 100644 index be114b69c..000000000 --- a/packages/snjs/mocha/migrations/2020-01-15-mobile.test.js +++ /dev/null @@ -1,1042 +0,0 @@ -/* eslint-disable no-undef */ -import * as Factory from '../lib/factory.js' -import * as Utils from '../lib/Utils.js' -import FakeWebCrypto from '../lib/fake_web_crypto.js' -chai.use(chaiAsPromised) -const expect = chai.expect - -describe('2020-01-15 mobile migration', () => { - beforeEach(() => { - localStorage.clear() - }) - - afterEach(() => { - localStorage.clear() - }) - - it( - '2020-01-15 migration with passcode and account', - async function () { - let application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'pc_params', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - const passcodeTiming = 'immediately' - - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - const customServer = 'http://server-dev.standardnotes.org' - await application.deviceInterface.setRawStorageValue( - 'user', - JSON.stringify({ email: identifier, server: customServer }), - ) - await application.deviceInterface.setLegacyRawKeychainValue({ - offline: { - pw: passcodeKey.serverPassword, - timing: passcodeTiming, - }, - }) - /** Wrap account key with passcode key and store in storage */ - const keyPayload = new DecryptedPayload({ - uuid: Utils.generateUuid(), - content_type: 'SN|Mobile|EncryptedKeys', - content: { - accountKeys: { - jwt: 'foo', - mk: accountKey.masterKey, - ak: accountKey.dataAuthenticationKey, - pw: accountKey.serverPassword, - }, - }, - }) - const encryptedKeyParams = await operator003.generateEncryptedParametersAsync(keyPayload, passcodeKey) - const wrappedKey = new EncryptedPayload({ ...keyPayload.ejected(), ...encryptedKeyParams }) - await application.deviceInterface.setRawStorageValue('encrypted_account_keys', JSON.stringify(wrappedKey)) - const biometricPrefs = { enabled: true, timing: 'immediately' } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - /** setup options */ - const lastExportDate = '2020:02' - await application.deviceInterface.setRawStorageValue('LastExportDateKey', lastExportDate) - const options = JSON.stringify({ - sortBy: 'userModifiedAt', - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: true, - hideDates: false, - hideTags: false, - }) - await application.deviceInterface.setRawStorageValue('options', options) - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if ( - prompt.validation === ChallengeValidation.None || - prompt.validation === ChallengeValidation.LocalPasscode - ) { - values.push(CreateChallengeValue(prompt, passcode)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - const values = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, values) - } - await application.prepareForLaunch({ - receiveChallenge, - }) - await application.launch(true) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(typeof keyParams).to.equal('object') - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - - const keychainValue = await application.deviceInterface.getNamespacedKeychainValue(application.identifier) - expect(keychainValue).to.not.be.ok - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - expect( - await application.diskStorageService.getValue(NonwrappedStorageKey.MobileFirstRun, StorageValueModes.Nonwrapped), - ).to.equal(false) - - expect( - await application.diskStorageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.enabled) - expect( - await application.diskStorageService.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.timing) - expect(await application.getUser().email).to.equal(identifier) - - const appId = application.identifier - console.warn('Expecting exception due to deiniting application while trying to renew session') - - /** Full sync completed event will not trigger due to mocked credentials, - * thus we manually need to mark any sync dependent migrations as complete. */ - await application.migrationService.markMigrationsAsDone() - await Factory.safeDeinit(application) - - /** Recreate application and ensure storage values are consistent */ - application = Factory.createApplicationWithFakeCrypto(appId) - await application.prepareForLaunch({ - receiveChallenge, - }) - await application.launch(true) - expect(await application.getUser().email).to.equal(identifier) - expect(await application.getHost()).to.equal(customServer) - const preferences = await application.diskStorageService.getValue('preferences') - expect(preferences.sortBy).to.equal('userModifiedAt') - expect(preferences.sortReverse).to.be.false - expect(preferences.hideDate).to.be.false - expect(preferences.hideTags).to.be.false - expect(preferences.hideNotePreview).to.be.true - expect(preferences.lastExportDate).to.equal(lastExportDate) - expect(preferences.doNotShowAgainUnsupportedEditors).to.be.false - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }, - Factory.TwentySecondTimeout, - ) - - it('2020-01-15 migration with passcode only', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'pc_params', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - const passcodeTiming = 'immediately' - await application.deviceInterface.setLegacyRawKeychainValue({ - offline: { - pw: passcodeKey.serverPassword, - timing: passcodeTiming, - }, - }) - - const biometricPrefs = { enabled: true, timing: 'immediately' } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - const passcodeKeyboardType = 'numeric' - await application.deviceInterface.setRawStorageValue('passcodeKeyboardType', passcodeKeyboardType) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, passcodeKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - /** setup options */ - await application.deviceInterface.setRawStorageValue('DoNotShowAgainUnsupportedEditorsKey', true) - const options = JSON.stringify({ - sortBy: undefined, - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: false, - hideDates: undefined, - hideTags: true, - }) - await application.deviceInterface.setRawStorageValue('options', options) - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None || prompt.validation === ChallengeValidation.LocalPasscode) { - values.push(CreateChallengeValue(prompt, passcode)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - await Factory.sleep(0) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.WrapperOnly) - await application.launch(true) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(passcodeKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(passcodeKey.dataAuthenticationKey) - /** Root key is in memory with passcode only, so server password can be defined */ - expect(rootKey.serverPassword).to.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.WrapperOnly) - - const keychainValue = await application.deviceInterface.getNamespacedKeychainValue(application.identifier) - expect(keychainValue).to.not.be.ok - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - expect( - await application.diskStorageService.getValue(NonwrappedStorageKey.MobileFirstRun, StorageValueModes.Nonwrapped), - ).to.equal(false) - expect( - await application.diskStorageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.enabled) - expect( - await application.diskStorageService.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.timing) - expect( - await application.diskStorageService.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped), - ).to.eql(passcodeTiming) - expect( - await application.diskStorageService.getValue(StorageKey.MobilePasscodeKeyboardType, StorageValueModes.Nonwrapped), - ).to.eql(passcodeKeyboardType) - const preferences = await application.diskStorageService.getValue('preferences') - expect(preferences.sortBy).to.equal(undefined) - expect(preferences.sortReverse).to.be.false - expect(preferences.hideNotePreview).to.be.false - expect(preferences.hideDate).to.be.false - expect(preferences.hideTags).to.be.true - expect(preferences.lastExportDate).to.equal(undefined) - expect(preferences.doNotShowAgainUnsupportedEditors).to.be.true - await Factory.safeDeinit(application) - }) - - it('2020-01-15 migration with passcode-only missing keychain', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'pc_params', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - const biometricPrefs = { enabled: true, timing: 'immediately' } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - const passcodeKeyboardType = 'numeric' - await application.deviceInterface.setRawStorageValue('passcodeKeyboardType', passcodeKeyboardType) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, passcodeKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - /** setup options */ - await application.deviceInterface.setRawStorageValue('DoNotShowAgainUnsupportedEditorsKey', true) - const options = JSON.stringify({ - sortBy: undefined, - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: false, - hideDates: undefined, - hideTags: true, - }) - await application.deviceInterface.setRawStorageValue('options', options) - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None || prompt.validation === ChallengeValidation.LocalPasscode) { - values.push(CreateChallengeValue(prompt, passcode)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - await Factory.sleep(0) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.WrapperOnly) - await application.launch(true) - - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.errorDecrypting).to.not.be.ok - - /** application should not crash */ - await Factory.safeDeinit(application) - }) - - it('2020-01-15 migration with account only', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: identifier })) - expect(accountKey.keyVersion).to.equal(ProtocolVersion.V003) - await application.deviceInterface.setLegacyRawKeychainValue({ - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - ak: accountKey.dataAuthenticationKey, - jwt: 'foo', - version: ProtocolVersion.V003, - }) - const biometricPrefs = { - enabled: true, - timing: 'immediately', - } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - /** setup options */ - const lastExportDate = '2020:02' - await application.deviceInterface.setRawStorageValue('LastExportDateKey', lastExportDate) - await application.deviceInterface.setRawStorageValue('DoNotShowAgainUnsupportedEditorsKey', false) - const options = JSON.stringify({ - sortBy: 'created_at', - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: true, - hideDates: false, - }) - await application.deviceInterface.setRawStorageValue('options', options) - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None) { - values.push(CreateChallengeValue(prompt, password)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - /** Runs migration */ - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - - const keyParams = await application.diskStorageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) - expect(typeof keyParams).to.equal('object') - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - expect( - await application.diskStorageService.getValue(NonwrappedStorageKey.MobileFirstRun, StorageValueModes.Nonwrapped), - ).to.equal(false) - expect( - await application.diskStorageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.enabled) - expect( - await application.diskStorageService.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.timing) - expect(await application.getUser().email).to.equal(identifier) - const preferences = await application.diskStorageService.getValue('preferences') - expect(preferences.sortBy).to.equal('created_at') - expect(preferences.sortReverse).to.be.false - expect(preferences.hideDate).to.be.false - expect(preferences.hideNotePreview).to.be.true - expect(preferences.lastExportDate).to.equal(lastExportDate) - expect(preferences.doNotShowAgainUnsupportedEditors).to.be.false - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 launching with account but missing keychain', async function () { - /** - * We expect that the keychain will attempt to be recovered - * We expect two challenges, one to recover just the keychain - * and another to recover the user session via a sign in request - */ - - /** Register a real user so we can attempt to sign back into this account later */ - const tempApp = await Factory.createInitAppWithFakeCrypto(Environment.Mobile, Platform.Ios) - const email = UuidGenerator.GenerateUuid() - const password = UuidGenerator.GenerateUuid() - /** Register with 003 account */ - await Factory.registerOldUser({ - application: tempApp, - email: email, - password: password, - version: ProtocolVersion.V003, - }) - const accountKey = tempApp.protocolService.getRootKey() - await Factory.safeDeinit(tempApp) - localStorage.clear() - - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - /** Create old version account parameters */ - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: email })) - expect(accountKey.keyVersion).to.equal(ProtocolVersion.V003) - - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.placeholder === SessionStrings.EmailInputPlaceholder) { - values.push(CreateChallengeValue(prompt, email)) - } else if (prompt.placeholder === SessionStrings.PasswordInputPlaceholder) { - values.push(CreateChallengeValue(prompt, password)) - } else { - throw Error('Unhandled prompt') - } - } - return values - } - let totalChallenges = 0 - const expectedChallenges = 2 - const receiveChallenge = async (challenge) => { - totalChallenges++ - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - - /** Recovery migration is non-blocking, so let's block for it */ - await Factory.sleep(1.0) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.be.ok - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - expect(await application.getUser().email).to.equal(email) - expect(await application.apiService.getSession()).to.be.ok - expect(totalChallenges).to.equal(expectedChallenges) - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 migration with 002 account should not create 003 data', async function () { - /** There was an issue where 002 account loading new app would create new default items key - * with 003 version. Should be 002. */ - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator002 = new SNProtocolOperator002(new FakeWebCrypto()) - const identifier = 'foo' - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator002.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: identifier })) - expect(accountKey.keyVersion).to.equal(ProtocolVersion.V002) - await application.deviceInterface.setLegacyRawKeychainValue({ - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - ak: accountKey.dataAuthenticationKey, - jwt: 'foo', - }) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator002.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None) { - values.push(CreateChallengeValue(prompt, password)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - - const itemsKey = application.itemManager.getDisplayableItemsKeys()[0] - expect(itemsKey.keyVersion).to.equal(ProtocolVersion.V002) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - expect(await application.getUser().email).to.equal(identifier) - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 migration with 001 account detect 001 version even with missing info', async function () { - /** If 001 account, and for some reason we dont have version stored, the migrations - * should determine correct version based on saved payloads */ - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator001 = new SNProtocolOperator001(new FakeWebCrypto()) - const identifier = 'foo' - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator001.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify({ - ...accountKey.keyParams.getPortableValue(), - version: undefined, - }), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: identifier })) - expect(accountKey.keyVersion).to.equal(ProtocolVersion.V001) - await application.deviceInterface.setLegacyRawKeychainValue({ - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - jwt: 'foo', - }) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator001.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None) { - values.push(CreateChallengeValue(prompt, password)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - - const itemsKey = application.itemManager.getDisplayableItemsKeys()[0] - expect(itemsKey.keyVersion).to.equal(ProtocolVersion.V001) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - expect(await application.getUser().email).to.equal(identifier) - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 successfully creates session if jwt is stored in keychain', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: identifier })) - - await application.deviceInterface.setLegacyRawKeychainValue({ - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - ak: accountKey.dataAuthenticationKey, - jwt: 'foo', - version: ProtocolVersion.V003, - }) - - await application.prepareForLaunch({ receiveChallenge: () => {} }) - await application.launch(true) - - expect(application.apiService.getSession()).to.be.ok - - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 successfully creates session if jwt is stored in storage', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - await application.deviceInterface.setRawStorageValue('user', JSON.stringify({ email: identifier, jwt: 'foo' })) - await application.deviceInterface.setLegacyRawKeychainValue({ - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - ak: accountKey.dataAuthenticationKey, - version: ProtocolVersion.V003, - }) - - await application.prepareForLaunch({ receiveChallenge: () => {} }) - await application.launch(true) - - expect(application.apiService.getSession()).to.be.ok - - await Factory.safeDeinit(application) - }).timeout(10000) - - it('2020-01-15 migration with no account and no passcode', async function () { - const application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const biometricPrefs = { - enabled: true, - timing: 'immediately', - } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - await application.deviceInterface.saveRawDatabasePayload(notePayload.ejected(), application.identifier) - /** setup options */ - await application.deviceInterface.setRawStorageValue('DoNotShowAgainUnsupportedEditorsKey', true) - const options = JSON.stringify({ - sortBy: 'created_at', - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: true, - hideDates: false, - }) - await application.deviceInterface.setRawStorageValue('options', options) - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.None || prompt.validation === ChallengeValidation.LocalPasscode) { - values.push(CreateChallengeValue(prompt, passcode)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyNone) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.not.be.ok - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyNone) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - expect( - await application.diskStorageService.getValue(NonwrappedStorageKey.MobileFirstRun, StorageValueModes.Nonwrapped), - ).to.equal(false) - expect( - await application.diskStorageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.enabled) - expect( - await application.diskStorageService.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.timing) - const preferences = await application.diskStorageService.getValue('preferences') - expect(preferences.sortBy).to.equal('created_at') - expect(preferences.sortReverse).to.be.false - expect(preferences.hideDate).to.be.false - expect(preferences.hideNotePreview).to.be.true - expect(preferences.lastExportDate).to.equal(undefined) - expect(preferences.doNotShowAgainUnsupportedEditors).to.be.true - await Factory.safeDeinit(application) - }) - - it( - '2020-01-15 migration from mobile version 3.0.16', - async function () { - /** - * In version 3.0.16, encrypted account keys were stored in keychain, not storage. - * This was migrated in version 3.0.17, but we want to be sure we can go from 3.0.16 - * to current state directly. - */ - let application = await Factory.createAppWithRandNamespace(Environment.Mobile, Platform.Ios) - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'pc_params', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - const passcodeTiming = 'immediately' - - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - await application.deviceInterface.setRawStorageValue( - 'auth_params', - JSON.stringify(accountKey.keyParams.getPortableValue()), - ) - const customServer = 'http://server-dev.standardnotes.org' - await application.deviceInterface.setRawStorageValue( - 'user', - JSON.stringify({ email: identifier, server: customServer }), - ) - /** Wrap account key with passcode key and store in storage */ - const keyPayload = new DecryptedPayload({ - uuid: Utils.generateUuid(), - content_type: 'SN|Mobile|EncryptedKeys', - content: { - accountKeys: { - jwt: 'foo', - mk: accountKey.masterKey, - ak: accountKey.dataAuthenticationKey, - pw: accountKey.serverPassword, - }, - }, - }) - const encryptedKeyParams = await operator003.generateEncryptedParametersAsync(keyPayload, passcodeKey) - const wrappedKey = new EncryptedPayload({ ...keyPayload, ...encryptedKeyParams }) - await application.deviceInterface.setLegacyRawKeychainValue({ - encryptedAccountKeys: wrappedKey, - offline: { - pw: passcodeKey.serverPassword, - timing: passcodeTiming, - }, - }) - const biometricPrefs = { enabled: true, timing: 'immediately' } - /** Create legacy storage. Storage in mobile was never wrapped. */ - await application.deviceInterface.setRawStorageValue('biometrics_prefs', JSON.stringify(biometricPrefs)) - await application.deviceInterface.setRawStorageValue(NonwrappedStorageKey.MobileFirstRun, false) - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - /** setup options */ - const lastExportDate = '2020:02' - await application.deviceInterface.setRawStorageValue('LastExportDateKey', lastExportDate) - const options = JSON.stringify({ - sortBy: 'userModifiedAt', - sortReverse: undefined, - selectedTagIds: [], - hidePreviews: true, - hideDates: false, - hideTags: false, - }) - await application.deviceInterface.setRawStorageValue('options', options) - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if ( - prompt.validation === ChallengeValidation.None || - prompt.validation === ChallengeValidation.LocalPasscode - ) { - values.push(CreateChallengeValue(prompt, passcode)) - } - if (prompt.validation === ChallengeValidation.Biometric) { - values.push(CreateChallengeValue(prompt, true)) - } - } - return values - } - const receiveChallenge = async (challenge) => { - const values = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, values) - } - await application.prepareForLaunch({ - receiveChallenge, - }) - await application.launch(true) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(typeof keyParams).to.equal('object') - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - - const keychainValue = await application.deviceInterface.getNamespacedKeychainValue(application.identifier) - expect(keychainValue).to.not.be.ok - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - expect( - await application.diskStorageService.getValue(NonwrappedStorageKey.MobileFirstRun, StorageValueModes.Nonwrapped), - ).to.equal(false) - - expect( - await application.diskStorageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.enabled) - expect( - await application.diskStorageService.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped), - ).to.equal(biometricPrefs.timing) - expect(await application.getUser().email).to.equal(identifier) - - const appId = application.identifier - console.warn('Expecting exception due to deiniting application while trying to renew session') - /** Full sync completed event will not trigger due to mocked credentials, - * thus we manually need to mark any sync dependent migrations as complete. */ - await application.migrationService.markMigrationsAsDone() - await Factory.safeDeinit(application) - - /** Recreate application and ensure storage values are consistent */ - application = Factory.createApplicationWithFakeCrypto(appId) - await application.prepareForLaunch({ - receiveChallenge, - }) - await application.launch(true) - expect(await application.getUser().email).to.equal(identifier) - expect(await application.getHost()).to.equal(customServer) - const preferences = await application.diskStorageService.getValue('preferences') - expect(preferences.sortBy).to.equal('userModifiedAt') - expect(preferences.sortReverse).to.be.false - expect(preferences.hideDate).to.be.false - expect(preferences.hideTags).to.be.false - expect(preferences.hideNotePreview).to.be.true - expect(preferences.lastExportDate).to.equal(lastExportDate) - expect(preferences.doNotShowAgainUnsupportedEditors).to.be.false - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }, - Factory.TwentySecondTimeout, - ) -}) diff --git a/packages/snjs/mocha/migrations/2020-01-15-web.test.js b/packages/snjs/mocha/migrations/2020-01-15-web.test.js deleted file mode 100644 index 5e3c131ac..000000000 --- a/packages/snjs/mocha/migrations/2020-01-15-web.test.js +++ /dev/null @@ -1,584 +0,0 @@ -/* eslint-disable no-unused-expressions */ -/* eslint-disable no-undef */ -import * as Factory from '../lib/factory.js' -import FakeWebCrypto from '../lib/fake_web_crypto.js' -chai.use(chaiAsPromised) -const expect = chai.expect - -describe('2020-01-15 web migration', () => { - beforeEach(() => { - localStorage.clear() - }) - - afterEach(() => { - localStorage.clear() - }) - - /** - * This test will pass but sync afterwards will not be successful - * as we are using a random value for the legacy session token - */ - it('2020-01-15 migration with passcode and account', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'offlineParams', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - - /** Create arbitrary storage values and make sure they're migrated */ - const arbitraryValues = { - foo: 'bar', - zar: 'tar', - har: 'car', - } - for (const key of Object.keys(arbitraryValues)) { - await application.deviceInterface.setRawStorageValue(key, arbitraryValues[key]) - } - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - - /** Create legacy storage and encrypt it with passcode */ - const embeddedStorage = { - mk: accountKey.masterKey, - ak: accountKey.dataAuthenticationKey, - pw: accountKey.serverPassword, - jwt: 'anything', - /** Legacy versions would store json strings inside of embedded storage */ - auth_params: JSON.stringify(accountKey.keyParams.getPortableValue()), - } - const storagePayload = new DecryptedPayload({ - uuid: await operator003.crypto.generateUUID(), - content_type: ContentType.EncryptedStorage, - content: { - storage: embeddedStorage, - }, - }) - const encryptionParams = await operator003.generateEncryptedParametersAsync(storagePayload, passcodeKey) - const persistPayload = new EncryptedPayload({ ...storagePayload, ...encryptionParams }) - await application.deviceInterface.setRawStorageValue('encryptedStorage', JSON.stringify(persistPayload)) - - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - await application.prepareForLaunch({ - receiveChallenge: async (challenge) => { - application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], passcode)]) - }, - }) - - await application.launch(true) - expect(application.sessionManager.online()).to.equal(true) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - expect(await application.deviceInterface.getRawStorageValue('offlineParams')).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) - expect(typeof keyParams).to.equal('object') - - /** Embedded value should match */ - const migratedKeyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(migratedKeyParams).to.eql(JSON.parse(embeddedStorage.auth_params)) - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - /** Application should not retain server password from legacy versions */ - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - /** Ensure arbitrary values have been migrated */ - for (const key of Object.keys(arbitraryValues)) { - const value = await application.diskStorageService.getValue(key) - expect(arbitraryValues[key]).to.equal(value) - } - - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }).timeout(15000) - - it('2020-01-15 migration with passcode only', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator003.createRootKey(identifier, passcode) - await application.deviceInterface.setRawStorageValue( - 'offlineParams', - JSON.stringify(passcodeKey.keyParams.getPortableValue()), - ) - - /** Create arbitrary storage values and make sure they're migrated */ - const arbitraryValues = { - foo: 'bar', - zar: 'tar', - har: 'car', - } - for (const key of Object.keys(arbitraryValues)) { - await application.deviceInterface.setRawStorageValue(key, arbitraryValues[key]) - } - - const embeddedStorage = { - ...arbitraryValues, - } - const storagePayload = new DecryptedPayload({ - uuid: await operator003.crypto.generateUUID(), - content: { - storage: embeddedStorage, - }, - content_type: ContentType.EncryptedStorage, - }) - const encryptionParams = await operator003.generateEncryptedParametersAsync(storagePayload, passcodeKey) - const persistPayload = new EncryptedPayload({ ...storagePayload, ...encryptionParams }) - await application.deviceInterface.setRawStorageValue('encryptedStorage', JSON.stringify(persistPayload)) - - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, passcodeKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - await application.prepareForLaunch({ - receiveChallenge: async (challenge) => { - application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], passcode)]) - }, - }) - await application.launch(true) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.WrapperOnly) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - - expect(await application.deviceInterface.getRawStorageValue('offlineParams')).to.not.be.ok - - /** Embedded value should match */ - const migratedKeyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(migratedKeyParams).to.eql(embeddedStorage.auth_params) - const rootKey = await application.protocolService.getRootKey() - expect(rootKey.masterKey).to.equal(passcodeKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(passcodeKey.dataAuthenticationKey) - /** Root key is in memory with passcode only, so server password can be defined */ - expect(rootKey.serverPassword).to.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.WrapperOnly) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - /** Ensure arbitrary values have been migrated */ - for (const key of Object.keys(arbitraryValues)) { - const value = await application.diskStorageService.getValue(key) - expect(arbitraryValues[key]).to.equal(value) - } - await Factory.safeDeinit(application) - }) - - /** - * This test will pass but sync afterwards will not be successful - * as we are using a random value for the legacy session token - */ - it('2020-01-15 migration with account only', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator003 = new SNProtocolOperator003(new FakeWebCrypto()) - const identifier = 'foo' - - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator003.createRootKey(identifier, password) - - /** Create arbitrary storage values and make sure they're migrated */ - const storage = { - foo: 'bar', - zar: 'tar', - har: 'car', - mk: accountKey.masterKey, - ak: accountKey.dataAuthenticationKey, - pw: accountKey.serverPassword, - jwt: 'anything', - /** Legacy versions would store json strings inside of embedded storage */ - auth_params: JSON.stringify(accountKey.keyParams.getPortableValue()), - } - for (const key of Object.keys(storage)) { - await application.deviceInterface.setRawStorageValue(key, storage[key]) - } - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator003.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - if (prompt.validation === ChallengeValidation.LocalPasscode) { - values.push(CreateChallengeValue(prompt, passcode)) - } else { - /** We will be prompted to reauthetnicate our session, not relevant to this test - * but pass any value to avoid exception - */ - values.push(CreateChallengeValue(prompt, 'foo')) - } - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - expect(application.sessionManager.online()).to.equal(true) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - /** Embedded value should match */ - const migratedKeyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(migratedKeyParams).to.eql(accountKey.keyParams.getPortableValue()) - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.be.ok - - expect(await application.deviceInterface.getRawStorageValue('migrations')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('auth_params')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('jwt')).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) - expect(typeof keyParams).to.equal('object') - - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V003) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - /** Ensure arbitrary values have been migrated */ - for (const key of Object.keys(storage)) { - /** Is stringified in storage, but parsed in storageService */ - if (key === 'auth_params') { - continue - } - const value = await application.diskStorageService.getValue(key) - expect(storage[key]).to.equal(value) - } - - console.warn('Expecting exception due to deiniting application while trying to renew session') - await Factory.safeDeinit(application) - }) - - it('2020-01-15 migration with no account and no passcode', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - /** Create arbitrary storage values and make sure they're migrated */ - const storage = { - foo: 'bar', - zar: 'tar', - har: 'car', - } - for (const key of Object.keys(storage)) { - await application.deviceInterface.setRawStorageValue(key, storage[key]) - } - - /** Create item and store it in db */ - const notePayload = Factory.createNotePayload() - await application.deviceInterface.saveRawDatabasePayload(notePayload.ejected(), application.identifier) - - /** Run migration */ - await application.prepareForLaunch({ - receiveChallenge: (_challenge) => { - return null - }, - }) - await application.launch(true) - - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyNone) - - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.not.be.ok - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyNone) - - expect(await application.deviceInterface.getRawStorageValue('migrations')).to.not.be.ok - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - /** Ensure arbitrary values have been migrated */ - for (const key of Object.keys(storage)) { - const value = await application.diskStorageService.getValue(key) - expect(storage[key]).to.equal(value) - } - - await Factory.safeDeinit(application) - }) - - /** - * This test will pass but sync afterwards will not be successful - * as we are using a random value for the legacy session token - */ - it('2020-01-15 migration from app v1.0.1 with account only', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator001 = new SNProtocolOperator001(new FakeWebCrypto()) - const identifier = 'foo' - - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator001.createRootKey(identifier, password) - - /** Create arbitrary storage values and make sure they're migrated */ - const storage = { - mk: accountKey.masterKey, - pw: accountKey.serverPassword, - jwt: 'anything', - /** Legacy versions would store json strings inside of embedded storage */ - auth_params: JSON.stringify(accountKey.keyParams.getPortableValue()), - user: JSON.stringify({ uuid: 'anything', email: 'anything' }), - } - for (const key of Object.keys(storage)) { - await application.deviceInterface.setRawStorageValue(key, storage[key]) - } - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator001.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Run migration */ - const promptValueReply = (prompts) => { - const values = [] - for (const prompt of prompts) { - /** We will be prompted to reauthetnicate our session, not relevant to this test - * but pass any value to avoid exception - */ - values.push(CreateChallengeValue(prompt, 'foo')) - } - return values - } - const receiveChallenge = async (challenge) => { - application.addChallengeObserver(challenge, { - onInvalidValue: (value) => { - const values = promptValueReply([value.prompt]) - application.submitValuesForChallenge(challenge, values) - }, - }) - const initialValues = promptValueReply(challenge.prompts) - application.submitValuesForChallenge(challenge, initialValues) - } - await application.prepareForLaunch({ - receiveChallenge: receiveChallenge, - }) - await application.launch(true) - expect(application.sessionManager.online()).to.equal(true) - expect(application.sessionManager.getUser()).to.be.ok - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - /** Embedded value should match */ - const migratedKeyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(migratedKeyParams).to.eql(accountKey.keyParams.getPortableValue()) - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.be.ok - - expect(await application.deviceInterface.getRawStorageValue('migrations')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('auth_params')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('jwt')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('ak')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('mk')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('pw')).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) - expect(typeof keyParams).to.equal('object') - - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V001) - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyOnly) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - /** Ensure arbitrary values have been migrated */ - for (const key of Object.keys(storage)) { - /** Is stringified in storage, but parsed in storageService */ - const value = await application.diskStorageService.getValue(key) - if (key === 'auth_params') { - continue - } else if (key === 'user') { - expect(storage[key]).to.equal(JSON.stringify(value)) - } else { - expect(storage[key]).to.equal(value) - } - } - await Factory.safeDeinit(application) - }) - - it('2020-01-15 migration from 002 app with account and passcode but missing offlineParams.version', async function () { - /** - * There was an issue where if the user had offlineParams but it was missing the version key, - * the user could not get past the passcode migration screen. - */ - const application = await Factory.createAppWithRandNamespace() - /** Create legacy migrations value so that base migration detects old app */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - const operator002 = new SNProtocolOperator002(new FakeWebCrypto()) - const identifier = 'foo' - const passcode = 'bar' - /** Create old version passcode parameters */ - const passcodeKey = await operator002.createRootKey(identifier, passcode) - - /** The primary chaos agent */ - const offlineParams = passcodeKey.keyParams.getPortableValue() - omitInPlace(offlineParams, ['version']) - - await application.deviceInterface.setRawStorageValue('offlineParams', JSON.stringify(offlineParams)) - - /** Create old version account parameters */ - const password = 'tar' - const accountKey = await operator002.createRootKey(identifier, password) - - /** Create legacy storage and encrypt it with passcode */ - const embeddedStorage = { - mk: accountKey.masterKey, - ak: accountKey.dataAuthenticationKey, - pw: accountKey.serverPassword, - jwt: 'anything', - /** Legacy versions would store json strings inside of embedded storage */ - auth_params: JSON.stringify(accountKey.keyParams.getPortableValue()), - user: JSON.stringify({ uuid: 'anything', email: 'anything' }), - } - const storagePayload = new DecryptedPayload({ - uuid: await operator002.crypto.generateUUID(), - content_type: ContentType.EncryptedStorage, - content: { - storage: embeddedStorage, - }, - }) - const encryptionParams = await operator002.generateEncryptedParametersAsync(storagePayload, passcodeKey) - const persistPayload = new EncryptedPayload({ ...storagePayload, ...encryptionParams }) - await application.deviceInterface.setRawStorageValue('encryptedStorage', JSON.stringify(persistPayload)) - - /** Create encrypted item and store it in db */ - const notePayload = Factory.createNotePayload() - const noteEncryptionParams = await operator002.generateEncryptedParametersAsync(notePayload, accountKey) - const noteEncryptedPayload = new EncryptedPayload({ ...notePayload, ...noteEncryptionParams }) - await application.deviceInterface.saveRawDatabasePayload(noteEncryptedPayload, application.identifier) - - /** Runs migration */ - await application.prepareForLaunch({ - receiveChallenge: async (challenge) => { - application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], passcode)]) - }, - }) - await application.launch(true) - expect(application.sessionManager.online()).to.equal(true) - expect(application.sessionManager.getUser()).to.be.ok - expect(application.protocolService.rootKeyEncryption.keyMode).to.equal(KeyMode.RootKeyPlusWrapper) - /** Should be decrypted */ - const storageMode = application.diskStorageService.domainKeyForMode(StorageValueModes.Default) - const valueStore = application.diskStorageService.values[storageMode] - expect(valueStore.content_type).to.not.be.ok - /** Embedded value should match */ - const migratedKeyParams = await application.diskStorageService.getValue( - StorageKey.RootKeyParams, - StorageValueModes.Nonwrapped, - ) - expect(migratedKeyParams).to.eql(accountKey.keyParams.getPortableValue()) - const rootKey = await application.protocolService.getRootKey() - expect(rootKey).to.be.ok - - expect(await application.deviceInterface.getRawStorageValue('migrations')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('auth_params')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('jwt')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('ak')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('mk')).to.not.be.ok - expect(await application.deviceInterface.getRawStorageValue('pw')).to.not.be.ok - - const keyParams = await application.diskStorageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) - expect(typeof keyParams).to.equal('object') - - expect(rootKey.masterKey).to.equal(accountKey.masterKey) - expect(rootKey.dataAuthenticationKey).to.equal(accountKey.dataAuthenticationKey) - expect(rootKey.serverPassword).to.not.be.ok - expect(rootKey.keyVersion).to.equal(ProtocolVersion.V002) - - /** Expect note is decrypted */ - expect(application.itemManager.getDisplayableNotes().length).to.equal(1) - const retrievedNote = application.itemManager.getDisplayableNotes()[0] - expect(retrievedNote.uuid).to.equal(notePayload.uuid) - expect(retrievedNote.content.text).to.equal(notePayload.content.text) - - await Factory.safeDeinit(application) - }) -}) diff --git a/packages/snjs/mocha/migrations/migration.test.js b/packages/snjs/mocha/migrations/migration.test.js index ae3066986..544fe566f 100644 --- a/packages/snjs/mocha/migrations/migration.test.js +++ b/packages/snjs/mocha/migrations/migration.test.js @@ -3,7 +3,7 @@ chai.use(chaiAsPromised) const expect = chai.expect describe('migrations', () => { - const allMigrations = ['2.0.0', '2.0.15', '2.7.0', '2.20.0', '2.36.0', '2.42.0'] + const allMigrations = ['2.0.15', '2.7.0', '2.20.0', '2.36.0', '2.42.0'] beforeEach(async () => { localStorage.clear() @@ -25,34 +25,13 @@ describe('migrations', () => { }) it('should return correct required migrations if stored version is 2.0.0', async function () { - expect((await SNMigrationService.getRequiredMigrations('2.0.0')).length).to.equal(allMigrations.length - 1) + expect((await SNMigrationService.getRequiredMigrations('2.0.0')).length).to.equal(allMigrations.length) }) it('should return 0 required migrations if stored version is futuristic', async function () { expect((await SNMigrationService.getRequiredMigrations('100.0.1')).length).to.equal(0) }) - it('after running base migration, legacy structure should set version as 1.0.0', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Set up 1.0.0 structure with tell-tale storage key */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - await application.migrationService.runBaseMigrationPreRun() - expect(await application.migrationService.getStoredSnjsVersion()).to.equal('1.0.0') - await Factory.safeDeinit(application) - }) - - it('after running base migration, 2.0.0 structure set version as 2.0.0', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Set up 2.0.0 structure with tell-tale storage key */ - await application.deviceInterface.setRawStorageValue( - namespacedKey(application.identifier, 'last_migration_timestamp'), - 'anything', - ) - await application.migrationService.runBaseMigrationPreRun() - expect(await application.migrationService.getStoredSnjsVersion()).to.equal('2.0.0') - await Factory.safeDeinit(application) - }) - it('after running base migration with no present storage values, should set version to current', async function () { const application = await Factory.createAppWithRandNamespace() await application.migrationService.runBaseMigrationPreRun() @@ -60,18 +39,6 @@ describe('migrations', () => { await Factory.safeDeinit(application) }) - it('after running all migrations from a 1.0.0 installation, should set stored version to current', async function () { - const application = await Factory.createAppWithRandNamespace() - /** Set up 1.0.0 structure with tell-tale storage key */ - await application.deviceInterface.setRawStorageValue('migrations', JSON.stringify(['anything'])) - await application.prepareForLaunch({ - receiveChallenge: () => {}, - }) - await application.launch(true) - expect(await application.migrationService.getStoredSnjsVersion()).to.equal(SnjsVersion) - await Factory.safeDeinit(application) - }) - it('after running all migrations from a 2.0.0 installation, should set stored version to current', async function () { const application = await Factory.createAppWithRandNamespace() /** Set up 2.0.0 structure with tell-tale storage key */ @@ -84,24 +51,6 @@ describe('migrations', () => { await Factory.safeDeinit(application) }) - it('should be correct migration count coming from 1.0.0', async function () { - const application = await Factory.createAppWithRandNamespace() - await application.deviceInterface.setRawStorageValue('migrations', 'anything') - await application.migrationService.runBaseMigrationPreRun() - expect(await application.migrationService.getStoredSnjsVersion()).to.equal('1.0.0') - const pendingMigrations = await SNMigrationService.getRequiredMigrations( - await application.migrationService.getStoredSnjsVersion(), - ) - expect(pendingMigrations.length).to.equal(allMigrations.length) - expect(pendingMigrations[0].version()).to.equal('2.0.0') - await application.prepareForLaunch({ - receiveChallenge: () => {}, - }) - await application.launch(true) - expect(await application.migrationService.getStoredSnjsVersion()).to.equal(SnjsVersion) - await Factory.safeDeinit(application) - }) - it('2.20.0 remove mfa migration', async function () { const application = await Factory.createAppWithRandNamespace() diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index 94e5d163c..2b9e41f74 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -735,7 +735,7 @@ describe('importing', function () { }), ) await application.deviceInterface.setRawStorageValue('standardnotes-snjs_version', '2.0.11') - await application.deviceInterface.saveRawDatabasePayload( + await application.deviceInterface.saveDatabaseEntry( { content: '003:9f2c7527eb8b2a1f8bfb3ea6b885403b6886bce2640843ebd57a6c479cbf7597:58e3322b-269a-4be3-a658-b035dffcd70f:9140b23a0fa989e224e292049f133154:SESTNOgIGf2+ZqmJdFnGU4EMgQkhKOzpZNoSzx76SJaImsayzctAgbUmJ+UU2gSQAHADS3+Z5w11bXvZgIrStTsWriwvYkNyyKmUPadKHNSBwOk4WeBZpWsA9gtI5zgI04Q5pvb8hS+kNW2j1DjM4YWqd0JQxMOeOrMIrxr/6Awn5TzYE+9wCbXZdYHyvRQcp9ui/G02ZJ67IA86vNEdjTTBAAWipWqTqKH9VDZbSQ2W/IOKfIquB373SFDKZb1S1NmBFvcoG2G7w//fAl/+ehYiL6UdiNH5MhXCDAOTQRFNfOh57HFDWVnz1VIp8X+VAPy6d9zzQH+8aws1JxHq/7BOhXrFE8UCueV6kERt9njgQxKJzd9AH32ShSiUB9X/sPi0fUXbS178xAZMJrNx3w==:eyJwd19ub25jZSI6IjRjYjEwM2FhODljZmY0NTYzYTkxMWQzZjM5NjU4M2NlZmM2ODMzYzY2Zjg4MGZiZWUwNmJkYTk0YzMxZjg2OGIiLCJwd19jb3N0IjoxMTAwMDAsImlkZW50aWZpZXIiOiJub3YyMzIyQGJpdGFyLmlvIiwidmVyc2lvbiI6IjAwMyIsIm9yaWdpbmF0aW9uIjoicmVnaXN0cmF0aW9uIn0=', diff --git a/packages/snjs/mocha/model_tests/mapping.test.js b/packages/snjs/mocha/model_tests/mapping.test.js index 0a26f880d..bef0c3d70 100644 --- a/packages/snjs/mocha/model_tests/mapping.test.js +++ b/packages/snjs/mocha/model_tests/mapping.test.js @@ -94,7 +94,7 @@ describe('model manager mapping', () => { const note = this.application.itemManager.getDisplayableNotes()[0] await this.application.itemManager.setItemDirty(note) const dirtyItems = this.application.itemManager.getDirtyItems() - expect(dirtyItems.length).to.equal(1) + expect(Uuids(dirtyItems).includes(note.uuid)) }) it('set all items dirty', async function () { diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index 688822ac6..32cb3ad34 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -642,7 +642,7 @@ describe('server session', function () { await app2Deinit const deviceInterface = new WebDeviceInterface() - const payloads = await deviceInterface.getAllRawDatabasePayloads(app2identifier) + const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) expect(payloads).to.be.empty }) @@ -670,7 +670,7 @@ describe('server session', function () { await app2Deinit const deviceInterface = new WebDeviceInterface() - const payloads = await deviceInterface.getAllRawDatabasePayloads(app2identifier) + const payloads = await deviceInterface.getAllDatabaseEntries(app2identifier) expect(payloads).to.be.empty }) diff --git a/packages/snjs/mocha/storage.test.js b/packages/snjs/mocha/storage.test.js index 5efdc786a..d18e3f4a9 100644 --- a/packages/snjs/mocha/storage.test.js +++ b/packages/snjs/mocha/storage.test.js @@ -300,6 +300,7 @@ describe('storage manager', function () { await Factory.createSyncedNote(this.application) expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems + 1) this.application = await Factory.signOutApplicationAndReturnNew(this.application) + await Factory.sleep(0.1, 'Allow all untrackable singleton syncs to complete') expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems) }) }) diff --git a/packages/snjs/mocha/sync_tests/offline.test.js b/packages/snjs/mocha/sync_tests/offline.test.js index e029a2702..037a2a3f7 100644 --- a/packages/snjs/mocha/sync_tests/offline.test.js +++ b/packages/snjs/mocha/sync_tests/offline.test.js @@ -31,10 +31,7 @@ describe('offline syncing', () => { it('should sync item with no passcode', async function () { let note = await Factory.createMappedNote(this.application) - expect(this.application.itemManager.getDirtyItems().length).to.equal(1) - - const rawPayloads1 = await this.application.diskStorageService.getAllRawPayloads() - expect(rawPayloads1.length).to.equal(this.expectedItemCount) + expect(Uuids(this.application.itemManager.getDirtyItems()).includes(note.uuid)) await this.application.syncService.sync(syncOptions) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 081d696ff..eea5e9f7d 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -218,14 +218,21 @@ describe('online syncing', function () { it('retrieving new items should not mark them as dirty', async function () { const originalNote = await Factory.createSyncedNote(this.application) this.expectedItemCount++ + this.application = await Factory.signOutApplicationAndReturnNew(this.application) - this.application.syncService.addEventObserver((event) => { - if (event === SyncEvent.SingleRoundTripSyncCompleted) { - const note = this.application.items.findItem(originalNote.uuid) - expect(note.dirty).to.not.be.ok - } + const promise = new Promise((resolve) => { + this.application.syncService.addEventObserver(async (event) => { + if (event === SyncEvent.SingleRoundTripSyncCompleted) { + const note = this.application.items.findItem(originalNote.uuid) + if (note) { + expect(note.dirty).to.not.be.ok + resolve() + } + } + }) }) await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true) + await promise }) it('allows saving of data after sign out', async function () { @@ -579,7 +586,7 @@ describe('online syncing', function () { await this.application.itemManager.setItemDirty(note) await this.application.syncService.sync(syncOptions) this.expectedItemCount++ - const rawPayloads = await this.application.syncService.getDatabasePayloads() + const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() const notePayload = rawPayloads.find((p) => p.content_type === ContentType.Note) expect(typeof notePayload.content).to.equal('string') }) @@ -651,8 +658,7 @@ describe('online syncing', function () { await this.application.syncService.clearSyncPositionTokens() await this.application.payloadManager.resetState() await this.application.itemManager.resetState() - const databasePayloads = await this.application.diskStorageService.getAllRawPayloads() - await this.application.syncService.loadDatabasePayloads(databasePayloads) + await this.application.syncService.loadDatabasePayloads() await this.application.syncService.sync(syncOptions) const newRawPayloads = await this.application.diskStorageService.getAllRawPayloads() @@ -672,7 +678,9 @@ describe('online syncing', function () { const payload = Factory.createStorageItemPayload(contentTypes[Math.floor(i / 2)]) originalPayloads.push(payload) } - const { contentTypePriorityPayloads } = GetSortedPayloadsByPriority(originalPayloads, ['C', 'A', 'B']) + const { contentTypePriorityPayloads } = GetSortedPayloadsByPriority(originalPayloads, { + contentTypePriority: ['C', 'A', 'B'], + }) expect(contentTypePriorityPayloads[0].content_type).to.equal('C') expect(contentTypePriorityPayloads[2].content_type).to.equal('A') expect(contentTypePriorityPayloads[4].content_type).to.equal('B') @@ -685,14 +693,10 @@ describe('online syncing', function () { await this.application.syncService.sync(syncOptions) this.application = await Factory.signOutApplicationAndReturnNew(this.application) - const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() - expect(rawPayloads.length).to.equal(BaseItemCounts.DefaultItems) - await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true) this.application.syncService.ut_setDatabaseLoaded(false) - const databasePayloads = await this.application.diskStorageService.getAllRawPayloads() - await this.application.syncService.loadDatabasePayloads(databasePayloads) + await this.application.syncService.loadDatabasePayloads() await this.application.syncService.sync(syncOptions) const items = await this.application.itemManager.items diff --git a/packages/snjs/mocha/test.html b/packages/snjs/mocha/test.html index b5d169c0b..e9bc11b6f 100644 --- a/packages/snjs/mocha/test.html +++ b/packages/snjs/mocha/test.html @@ -80,8 +80,6 @@ - - diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 30861ab48..0d25c2949 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -22,6 +22,7 @@ "clean": "rm -fr dist", "prebuild": "yarn clean", "build": "yarn tsc && webpack --config webpack.prod.js", + "watch": "webpack --config webpack.prod.js --watch", "docs": "jsdoc -c jsdoc.json", "tsc": "tsc --project lib/tsconfig.json && tscpaths -p lib/tsconfig.json -s lib -o dist/@types", "lint": "yarn lint:eslint lib", diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index e93b9ab54..efd05875b 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -21,6 +21,8 @@ import { DecryptedItem, EditorIdentifier, FeatureIdentifier, + Environment, + ApplicationOptionsDefaults, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' import { PanelResizedData } from '@/Types/PanelResizedData' @@ -75,6 +77,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter defaultHost: defaultSyncServerHost, appVersion: deviceInterface.appVersion, webSocketUrl: webSocketUrl, + loadBatchSize: + deviceInterface.environment === Environment.Mobile ? 100 : ApplicationOptionsDefaults.loadBatchSize, }) makeObservable(this, { diff --git a/packages/web/src/javascripts/Application/Database.ts b/packages/web/src/javascripts/Application/Database.ts index a5ed63d40..9482041f5 100644 --- a/packages/web/src/javascripts/Application/Database.ts +++ b/packages/web/src/javascripts/Application/Database.ts @@ -140,6 +140,39 @@ export class Database { }) } + /** + * This function is actually unused, but implemented to conform to protocol in case it is eventually needed. + * We could remove implementation and throw instead, but it might be better to offer a functional alternative instead. + */ + public async getPayloadsForKeys(keys: string[]): Promise { + const db = (await this.openDatabase()) as IDBDatabase + return new Promise((resolve) => { + const objectStore = db.transaction(STORE_NAME).objectStore(STORE_NAME) + const payloads: any = [] + let numComplete = 0 + for (const key of keys) { + const getRequest = objectStore.get(key) + getRequest.onsuccess = (event) => { + const target = event.target as any + const result = target.result + if (result) { + payloads.push(result) + } + numComplete++ + if (numComplete === keys.length) { + resolve(payloads) + } + } + getRequest.onerror = () => { + numComplete++ + if (numComplete === keys.length) { + resolve(payloads) + } + } + } + }) + } + public async getAllKeys(): Promise { const db = (await this.openDatabase()) as IDBDatabase diff --git a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts index 938aa0c98..e4cc3b428 100644 --- a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts +++ b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts @@ -2,13 +2,16 @@ import { SNApplication, ApplicationIdentifier, Environment, - LegacyRawKeychainValue, RawKeychainValue, TransferPayload, NamespacedRootKeyInKeychain, - extendArray, WebOrDesktopDeviceInterface, Platform, + FullyFormedTransferPayload, + DatabaseLoadOptions, + GetSortedPayloadsByPriority, + DatabaseFullEntryLoadChunk, + DatabaseFullEntryLoadChunkResponse, } from '@standardnotes/snjs' import { Database } from '../Database' @@ -72,17 +75,6 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface return result } - async getAllRawStorageKeyValues() { - const results = [] - for (const key of Object.keys(localStorage)) { - results.push({ - key: key, - value: localStorage[key], - }) - } - return results - } - async setRawStorageValue(key: string, value: string) { localStorage.setItem(key, value) } @@ -111,23 +103,63 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface }) as Promise<{ isNewDatabase?: boolean } | undefined> } - async getAllRawDatabasePayloads(identifier: ApplicationIdentifier) { + async getDatabaseLoadChunks( + options: DatabaseLoadOptions, + identifier: string, + ): Promise { + const entries = await this.getAllDatabaseEntries(identifier) + const sorted = GetSortedPayloadsByPriority(entries, options) + + const itemsKeysChunk: DatabaseFullEntryLoadChunk = { + entries: sorted.itemsKeyPayloads, + } + + const contentTypePriorityChunk: DatabaseFullEntryLoadChunk = { + entries: sorted.contentTypePriorityPayloads, + } + + const remainingPayloadsChunks: DatabaseFullEntryLoadChunk[] = [] + for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) { + remainingPayloadsChunks.push({ + entries: sorted.remainingPayloads.slice(i, i + options.batchSize), + }) + } + + const result: DatabaseFullEntryLoadChunkResponse = { + fullEntries: { + itemsKeys: itemsKeysChunk, + remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks], + }, + remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + } + + return result + } + + async getAllDatabaseEntries(identifier: ApplicationIdentifier) { return this.databaseForIdentifier(identifier).getAllPayloads() } - async saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier) { + getDatabaseEntries( + identifier: string, + keys: string[], + ): Promise { + return this.databaseForIdentifier(identifier).getPayloadsForKeys(keys) + } + + async saveDatabaseEntry(payload: TransferPayload, identifier: ApplicationIdentifier) { return this.databaseForIdentifier(identifier).savePayload(payload) } - async saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier) { + async saveDatabaseEntries(payloads: TransferPayload[], identifier: ApplicationIdentifier) { return this.databaseForIdentifier(identifier).savePayloads(payloads) } - async removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier) { + async removeDatabaseEntry(id: string, identifier: ApplicationIdentifier) { return this.databaseForIdentifier(identifier).deletePayload(id) } - async removeAllRawDatabasePayloads(identifier: ApplicationIdentifier) { + async removeAllDatabaseEntries(identifier: ApplicationIdentifier) { return this.databaseForIdentifier(identifier).clearAllPayloads() } @@ -141,16 +173,6 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface return keychain[identifier] } - async getDatabaseKeys(): Promise { - const keys: string[] = [] - - for (const database of this.databases) { - extendArray(keys, await database.getAllKeys()) - } - - return keys - } - async setNamespacedKeychainValue(value: NamespacedRootKeyInKeychain, identifier: ApplicationIdentifier) { let keychain = await this.getKeychainValue() @@ -186,10 +208,6 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface } } - setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise { - return this.setKeychainValue(value) - } - abstract getKeychainValue(): Promise abstract setKeychainValue(value: unknown): Promise diff --git a/yarn.lock b/yarn.lock index 686e056e6..c6b6eec89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5883,6 +5883,7 @@ __metadata: react-native-fs: ^2.20.0 react-native-iap: ^12.4.4 react-native-keychain: "standardnotes/react-native-keychain#d277d360494cbd02be4accb4a360772a8e0e97b6" + react-native-mmkv: ^2.5.1 react-native-privacy-snapshot: "standardnotes/react-native-privacy-snapshot#653e904c90fc6f2b578da59138f2bfe5d7f942fe" react-native-share: ^8.0.0 react-native-version-info: ^1.1.1 @@ -25615,6 +25616,16 @@ __metadata: languageName: node linkType: hard +"react-native-mmkv@npm:^2.5.1": + version: 2.5.1 + resolution: "react-native-mmkv@npm:2.5.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 6f0cf484e71d8069c9b3cdb57b76eafaca40aa75f359beb6959c77d0ef66d0481d4459b1ffa94640170ce4744e337fefb38b8ccf6e1a3c3663561ede5f7a2c20 + languageName: node + linkType: hard + "react-native-privacy-snapshot@standardnotes/react-native-privacy-snapshot#653e904c90fc6f2b578da59138f2bfe5d7f942fe": version: 1.0.0 resolution: "react-native-privacy-snapshot@https://github.com/standardnotes/react-native-privacy-snapshot.git#commit=653e904c90fc6f2b578da59138f2bfe5d7f942fe"