diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index e7a56fbef..81c712661 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -12,13 +12,14 @@ import { StatusServiceInterface, MfaServiceInterface, GenerateUuid, + CreateDecryptedBackupFile, } from '@standardnotes/services' import { VaultLockServiceInterface } from './../VaultLock/VaultLockServiceInterface' import { HistoryServiceInterface } from './../History/HistoryServiceInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface' import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface' -import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' +import { ImportDataResult } from '../Import/ImportDataResult' import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' import { VaultServiceInterface } from '../Vault/VaultServiceInterface' import { ApplicationIdentifier } from '@standardnotes/common' @@ -42,6 +43,8 @@ import { UserServiceInterface } from '../User/UserServiceInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { Result } from '@standardnotes/domain-core' +import { CreateEncryptedBackupFile } from '../Import/CreateEncryptedBackupFile' export interface ApplicationInterface { deinit(mode: DeinitMode, source: DeinitSource): void @@ -51,9 +54,6 @@ export interface ApplicationInterface { addEventObserver(callback: ApplicationEventCallback, singleEvent?: ApplicationEvent): () => void addSingleEventObserver(event: ApplicationEvent, callback: ApplicationEventCallback): () => void hasProtectionSources(): boolean - createEncryptedBackupFileForAutomatedDesktopBackups(): Promise - createEncryptedBackupFile(): Promise - createDecryptedBackupFile(): Promise hasPasscode(): boolean lock(): Promise setValue(key: string, value: unknown, mode?: StorageValueModes): void @@ -68,12 +68,17 @@ export interface ApplicationInterface { setCustomHost(host: string): Promise isUsingHomeServer(): Promise - importData(data: BackupFile, awaitSync?: boolean): Promise + importData(data: BackupFile, awaitSync?: boolean): Promise> + // Use cases get changeAndSaveItem(): ChangeAndSaveItem + get createDecryptedBackupFile(): CreateDecryptedBackupFile + get createEncryptedBackupFile(): CreateEncryptedBackupFile + get generateUuid(): GenerateUuid get getHost(): GetHost get setHost(): SetHost + // Services get alerts(): AlertService get asymmetric(): AsymmetricMessageServiceInterface get challenges(): ChallengeServiceInterface @@ -85,7 +90,6 @@ export interface ApplicationInterface { get files(): FilesClientInterface get history(): HistoryServiceInterface get homeServer(): HomeServerServiceInterface | undefined - get generateUuid(): GenerateUuid get items(): ItemManagerInterface get legacyApi(): LegacyApiServiceInterface get mfa(): MfaServiceInterface diff --git a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts index 529caf5a6..78fd9d848 100644 --- a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts +++ b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts @@ -8,7 +8,6 @@ import { } from '@standardnotes/encryption' import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' import { - BackupFile, DecryptedPayloadInterface, EncryptedPayloadInterface, ItemContent, @@ -54,9 +53,6 @@ export interface EncryptionProviderInterface { getEncryptionDisplayName(): Promise upgradeAvailable(): Promise - createEncryptedBackupFile(): Promise - createDecryptedBackupFile(): BackupFile - getUserVersion(): ProtocolVersion | undefined hasAccount(): boolean hasPasscode(): boolean @@ -75,6 +71,11 @@ export interface EncryptionProviderInterface { deleteWorkspaceSpecificKeyStateFromDevice(): Promise + itemsKeyForEncryptedPayload( + payload: EncryptedPayloadInterface, + ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined + defaultItemsKeyForItemVersion(version: ProtocolVersion, fromKeys?: ItemsKeyInterface[]): ItemsKeyInterface | undefined + unwrapRootKey(wrappingKey: RootKeyInterface): Promise computeRootKey(password: string, keyParams: SNRootKeyParams): Promise computeWrappingKey(passcode: string): Promise diff --git a/packages/services/src/Domain/Encryption/EncryptionService.ts b/packages/services/src/Domain/Encryption/EncryptionService.ts index b19b53678..ea3ecb942 100644 --- a/packages/services/src/Domain/Encryption/EncryptionService.ts +++ b/packages/services/src/Domain/Encryption/EncryptionService.ts @@ -5,7 +5,6 @@ import { InternalEventHandlerInterface } from './../Internal/InternalEventHandle import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { CreateAnyKeyParams, - CreateEncryptionSplitWithKeyLookup, encryptedInputParametersFromPayload, ErrorDecryptingParameters, FindPayloadInDecryptionSplit, @@ -16,7 +15,6 @@ import { KeyedEncryptionSplit, LegacyAttachedData, RootKeyEncryptedAuthenticatedData, - SplitPayloadsByEncryptionType, V001Algorithm, V002Algorithm, EncryptedOutputParameters, @@ -25,15 +23,10 @@ import { EncryptionOperatorsInterface, } from '@standardnotes/encryption' import { - BackupFile, - CreateDecryptedBackupFileContextPayload, - CreateEncryptedBackupFileContextPayload, DecryptedPayload, DecryptedPayloadInterface, EncryptedPayload, EncryptedPayloadInterface, - isDecryptedPayload, - isEncryptedPayload, ItemContent, ItemsKeyInterface, RootKeyInterface, @@ -47,7 +40,6 @@ import { import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { extendArray, - isNotUndefined, isNullOrUndefined, isReactNativeEnvironment, isWebCryptoAvailable, @@ -81,7 +73,6 @@ import { EncryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/EncryptPayload import { EncryptTypeAPayload } from './UseCase/TypeA/EncryptPayload' import { ValidateAccountPasswordResult } from '../RootKeyManager/ValidateAccountPasswordResult' import { ValidatePasscodeResult } from '../RootKeyManager/ValidatePasscodeResult' -import { ContentType } from '@standardnotes/domain-core' import { EncryptionProviderInterface } from './EncryptionProviderInterface' import { KeyMode } from '../RootKeyManager/KeyMode' @@ -578,47 +569,6 @@ export class EncryptionService return CreateAnyKeyParams(keyParams) } - public async createEncryptedBackupFile(): Promise { - const payloads = this.items.items.map((item) => item.payload) - - const split = SplitPayloadsByEncryptionType(payloads) - - const keyLookupSplit = CreateEncryptionSplitWithKeyLookup(split) - - const result = await this.encryptSplit(keyLookupSplit) - - const ejected = result.map((payload) => CreateEncryptedBackupFileContextPayload(payload)) - - const data: BackupFile = { - version: ProtocolVersionLatest, - items: ejected, - } - - const keyParams = this.getRootKeyParams() - data.keyParams = keyParams?.getPortableValue() - return data - } - - public createDecryptedBackupFile(): BackupFile { - const payloads = this.payloads.nonDeletedItems.filter((item) => item.content_type !== ContentType.TYPES.ItemsKey) - - const data: BackupFile = { - version: ProtocolVersionLatest, - items: payloads - .map((payload) => { - if (isDecryptedPayload(payload)) { - return CreateDecryptedBackupFileContextPayload(payload) - } else if (isEncryptedPayload(payload)) { - return CreateEncryptedBackupFileContextPayload(payload) - } - return undefined - }) - .filter(isNotUndefined), - } - - return data - } - public hasPasscode(): boolean { return this.rootKeyManager.hasPasscode() } diff --git a/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts b/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts deleted file mode 100644 index a93288a0f..000000000 --- a/packages/services/src/Domain/Encryption/UseCase/DecryptBackupFile.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { - AnyKeyParamsContent, - compareVersions, - leftVersionGreaterThanOrEqualToRight, - ProtocolVersion, -} from '@standardnotes/common' -import { - BackupFileType, - CreateAnyKeyParams, - isItemsKey, - isKeySystemItemsKey, - SNItemsKey, - SplitPayloadsByEncryptionType, -} from '@standardnotes/encryption' -import { - ContentTypeUsesKeySystemRootKeyEncryption, - ContentTypeUsesRootKeyEncryption, - BackupFile, - CreateDecryptedItemFromPayload, - CreatePayloadSplit, - DecryptedPayload, - DecryptedPayloadInterface, - EncryptedPayload, - EncryptedPayloadInterface, - isDecryptedPayload, - isDecryptedTransferPayload, - isEncryptedPayload, - isEncryptedTransferPayload, - ItemsKeyContent, - ItemsKeyInterface, - PayloadInterface, - KeySystemItemsKeyInterface, - RootKeyInterface, - KeySystemRootKeyInterface, - isKeySystemRootKey, - RootKeyParamsInterface, -} from '@standardnotes/models' -import { ClientDisplayableError } from '@standardnotes/responses' -import { extendArray, LoggerInterface } from '@standardnotes/utils' -import { EncryptionService } from '../EncryptionService' -import { ContentType } from '@standardnotes/domain-core' - -export class DecryptBackupFile { - constructor( - private encryption: EncryptionService, - private logger: LoggerInterface, - ) {} - - async execute( - file: BackupFile, - password?: string, - ): Promise { - const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => { - if (isEncryptedTransferPayload(item)) { - return new EncryptedPayload(item) - } else if (isDecryptedTransferPayload(item)) { - return new DecryptedPayload(item) - } else { - throw Error('Unhandled case in DecryptBackupFile') - } - }) - - const { encrypted, decrypted } = CreatePayloadSplit(payloads) - - const type = this.getBackupFileType(file, payloads) - - switch (type) { - case BackupFileType.Corrupt: - return new ClientDisplayableError('Invalid backup file.') - case BackupFileType.Encrypted: { - if (!password) { - throw Error('Attempting to decrypt encrypted file with no password') - } - - const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent - - const rootKey = await this.encryption.computeRootKey(password, CreateAnyKeyParams(keyParamsData)) - - const results = await this.decryptEncrypted({ - password, - payloads: encrypted, - rootKey, - keyParams: CreateAnyKeyParams(keyParamsData), - }) - - return [...decrypted, ...results] - } - case BackupFileType.EncryptedWithNonEncryptedItemsKey: - return [...decrypted, ...(await this.decryptEncryptedWithNonEncryptedItemsKey(payloads))] - case BackupFileType.FullyDecrypted: - return [...decrypted, ...encrypted] - } - } - - private getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType { - if (file.keyParams || file.auth_params) { - return BackupFileType.Encrypted - } else { - const hasEncryptedItem = payloads.find(isEncryptedPayload) - const hasDecryptedItemsKey = payloads.find( - (payload) => payload.content_type === ContentType.TYPES.ItemsKey && isDecryptedPayload(payload), - ) - - if (hasEncryptedItem && hasDecryptedItemsKey) { - return BackupFileType.EncryptedWithNonEncryptedItemsKey - } else if (!hasEncryptedItem) { - return BackupFileType.FullyDecrypted - } else { - return BackupFileType.Corrupt - } - } - } - - private async decryptEncrypted(dto: { - password: string - keyParams: RootKeyParamsInterface - payloads: EncryptedPayloadInterface[] - rootKey: RootKeyInterface - }): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { - const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = [] - - const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(dto.payloads) - - const rootKeyBasedDecryptionResults = await this.encryption.decryptSplit({ - usesRootKey: { - items: rootKeyEncryption || [], - key: dto.rootKey, - }, - }) - - extendArray(results, rootKeyBasedDecryptionResults) - - const decryptedPayloads = await this.decrypt({ - payloads: itemsKeyEncryption || [], - availableItemsKeys: rootKeyBasedDecryptionResults - .filter(isItemsKey) - .filter(isDecryptedPayload) - .map((p) => CreateDecryptedItemFromPayload(p)), - keyParams: dto.keyParams, - rootKey: dto.rootKey, - }) - - extendArray(results, decryptedPayloads) - - return results - } - - private async decryptEncryptedWithNonEncryptedItemsKey( - payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[], - ): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { - const decryptedItemsKeys: DecryptedPayloadInterface[] = [] - const encryptedPayloads: EncryptedPayloadInterface[] = [] - - payloads.forEach((payload) => { - if (payload.content_type === ContentType.TYPES.ItemsKey && isDecryptedPayload(payload)) { - decryptedItemsKeys.push(payload as DecryptedPayloadInterface) - } else if (isEncryptedPayload(payload)) { - encryptedPayloads.push(payload) - } - }) - - const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload(p)) - - return this.decrypt({ payloads: encryptedPayloads, availableItemsKeys: itemsKeys, rootKey: undefined }) - } - - private findKeyToUseForPayload(dto: { - payload: EncryptedPayloadInterface - availableKeys: ItemsKeyInterface[] - keyParams?: RootKeyParamsInterface - rootKey?: RootKeyInterface - }): ItemsKeyInterface | RootKeyInterface | KeySystemRootKeyInterface | KeySystemItemsKeyInterface | undefined { - if (ContentTypeUsesRootKeyEncryption(dto.payload.content_type)) { - if (!dto.rootKey) { - throw new Error('Attempting to decrypt root key encrypted payload with no root key') - } - return dto.rootKey - } - - if (ContentTypeUsesKeySystemRootKeyEncryption(dto.payload.content_type)) { - throw new Error('Backup file key system root key encryption is not supported') - } - - let itemsKey: ItemsKeyInterface | RootKeyInterface | KeySystemItemsKeyInterface | undefined - - if (dto.payload.items_key_id) { - itemsKey = this.encryption.itemsKeyForEncryptedPayload(dto.payload) - if (itemsKey) { - return itemsKey - } - } - - itemsKey = dto.availableKeys.find((itemsKeyPayload) => { - return dto.payload.items_key_id === itemsKeyPayload.uuid - }) - - if (itemsKey) { - return itemsKey - } - - if (!dto.keyParams) { - return undefined - } - - const payloadVersion = dto.payload.version as ProtocolVersion - - /** - * Payloads with versions <= 003 use root key directly for encryption. - * However, if the incoming key params are >= 004, this means we should - * have an items key based off the 003 root key. We can't use the 004 - * root key directly because it's missing dataAuthenticationKey. - */ - if (leftVersionGreaterThanOrEqualToRight(dto.keyParams.version, ProtocolVersion.V004)) { - itemsKey = this.encryption.defaultItemsKeyForItemVersion(payloadVersion, dto.availableKeys) - } else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) { - itemsKey = dto.rootKey - } - - return itemsKey - } - - private async decrypt(dto: { - payloads: EncryptedPayloadInterface[] - availableItemsKeys: ItemsKeyInterface[] - rootKey: RootKeyInterface | undefined - keyParams?: RootKeyParamsInterface - }): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> { - const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = [] - - for (const encryptedPayload of dto.payloads) { - try { - const key = this.findKeyToUseForPayload({ - payload: encryptedPayload, - availableKeys: dto.availableItemsKeys, - keyParams: dto.keyParams, - rootKey: dto.rootKey, - }) - - if (!key) { - results.push( - encryptedPayload.copy({ - errorDecrypting: true, - }), - ) - continue - } - - if (isItemsKey(key) || isKeySystemItemsKey(key)) { - const decryptedPayload = await this.encryption.decryptSplitSingle({ - usesItemsKey: { - items: [encryptedPayload], - key: key, - }, - }) - results.push(decryptedPayload) - } else if (isKeySystemRootKey(key)) { - const decryptedPayload = await this.encryption.decryptSplitSingle({ - usesKeySystemRootKey: { - items: [encryptedPayload], - key: key, - }, - }) - results.push(decryptedPayload) - } else { - const decryptedPayload = await this.encryption.decryptSplitSingle({ - usesRootKey: { - items: [encryptedPayload], - key: key, - }, - }) - results.push(decryptedPayload) - } - } catch (e) { - results.push( - encryptedPayload.copy({ - errorDecrypting: true, - }), - ) - this.logger.error('Error decrypting payload', encryptedPayload, e) - } - } - - return results - } -} diff --git a/packages/services/src/Domain/Import/CreateDecryptedBackupFile.ts b/packages/services/src/Domain/Import/CreateDecryptedBackupFile.ts new file mode 100644 index 000000000..23e8b42aa --- /dev/null +++ b/packages/services/src/Domain/Import/CreateDecryptedBackupFile.ts @@ -0,0 +1,43 @@ +import { ProtectionsClientInterface } from './../Protection/ProtectionClientInterface' +import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core' +import { + BackupFile, + CreateDecryptedBackupFileContextPayload, + CreateEncryptedBackupFileContextPayload, + isDecryptedPayload, + isEncryptedPayload, +} from '@standardnotes/models' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { ProtocolVersionLatest } from '@standardnotes/common' +import { isNotUndefined } from '@standardnotes/utils' + +export class CreateDecryptedBackupFile implements UseCaseInterface { + constructor( + private payloads: PayloadManagerInterface, + private protections: ProtectionsClientInterface, + ) {} + + async execute(): Promise> { + if (!(await this.protections.authorizeBackupCreation())) { + return Result.fail('Failed to authorize backup creation') + } + + const payloads = this.payloads.nonDeletedItems.filter((item) => item.content_type !== ContentType.TYPES.ItemsKey) + + const data: BackupFile = { + version: ProtocolVersionLatest, + items: payloads + .map((payload) => { + if (isDecryptedPayload(payload)) { + return CreateDecryptedBackupFileContextPayload(payload) + } else if (isEncryptedPayload(payload)) { + return CreateEncryptedBackupFileContextPayload(payload) + } + return undefined + }) + .filter(isNotUndefined), + } + + return Result.ok(data) + } +} diff --git a/packages/services/src/Domain/Import/CreateEncryptedBackupFile.ts b/packages/services/src/Domain/Import/CreateEncryptedBackupFile.ts new file mode 100644 index 000000000..26f398e6b --- /dev/null +++ b/packages/services/src/Domain/Import/CreateEncryptedBackupFile.ts @@ -0,0 +1,40 @@ +import { ItemManagerInterface } from './../Item/ItemManagerInterface' +import { ProtectionsClientInterface } from './../Protection/ProtectionClientInterface' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { BackupFile, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models' +import { ProtocolVersionLatest } from '@standardnotes/common' +import { CreateEncryptionSplitWithKeyLookup, SplitPayloadsByEncryptionType } from '@standardnotes/encryption' +import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' + +export class CreateEncryptedBackupFile implements UseCaseInterface { + constructor( + private items: ItemManagerInterface, + private protections: ProtectionsClientInterface, + private encryption: EncryptionProviderInterface, + ) {} + + async execute(params: { skipAuthorization: boolean } = { skipAuthorization: false }): Promise> { + if (!params.skipAuthorization && !(await this.protections.authorizeBackupCreation())) { + return Result.fail('Failed to authorize backup creation') + } + + const payloads = this.items.items.map((item) => item.payload) + + const split = SplitPayloadsByEncryptionType(payloads) + + const keyLookupSplit = CreateEncryptionSplitWithKeyLookup(split) + + const result = await this.encryption.encryptSplit(keyLookupSplit) + + const ejected = result.map((payload) => CreateEncryptedBackupFileContextPayload(payload)) + + const data: BackupFile = { + version: ProtocolVersionLatest, + items: ejected, + } + + const keyParams = this.encryption.getRootKeyParams() + data.keyParams = keyParams?.getPortableValue() + return Result.ok(data) + } +} diff --git a/packages/services/src/Domain/Import/DecryptBackupFile.ts b/packages/services/src/Domain/Import/DecryptBackupFile.ts new file mode 100644 index 000000000..49c666e99 --- /dev/null +++ b/packages/services/src/Domain/Import/DecryptBackupFile.ts @@ -0,0 +1,215 @@ +import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface' +import { AnyKeyParamsContent } from '@standardnotes/common' +import { + BackupFileType, + CreateAnyKeyParams, + isItemsKey, + isKeySystemItemsKey, + SNItemsKey, + SplitPayloadsByEncryptionType, +} from '@standardnotes/encryption' +import { + BackupFile, + CreateDecryptedItemFromPayload, + CreatePayloadSplit, + DecryptedPayload, + DecryptedPayloadInterface, + EncryptedPayload, + EncryptedPayloadInterface, + isDecryptedPayload, + isDecryptedTransferPayload, + isEncryptedPayload, + isEncryptedTransferPayload, + isKeySystemRootKey, + ItemsKeyContent, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, +} from '@standardnotes/models' +import { extendArray } from '@standardnotes/utils' +import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core' +import { GetBackupFileType } from './GetBackupFileType' +import { DecryptBackupPayloads } from './DecryptBackupPayloads' +import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface' + +export class DecryptBackupFile implements UseCaseInterface<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { + constructor( + private encryption: EncryptionProviderInterface, + private keys: KeySystemKeyManagerInterface, + private _getBackupFileType: GetBackupFileType, + private _decryptBackupPayloads: DecryptBackupPayloads, + ) {} + + async execute( + file: BackupFile, + password?: string, + ): Promise> { + const payloads = this.convertToPayloads(file) + + const type = this._getBackupFileType.execute(file, payloads).getValue() + + if (type === BackupFileType.Corrupt) { + return Result.fail('Invalid backup file.') + } + + const { encrypted, decrypted } = CreatePayloadSplit(payloads) + + if (type === BackupFileType.FullyDecrypted) { + return Result.ok([...decrypted, ...encrypted]) + } + + if (type === BackupFileType.EncryptedWithNonEncryptedItemsKey) { + const result = await this.handleEncryptedWithNonEncryptedItemsKeyFileType(payloads) + if (result.isFailed()) { + return Result.fail(result.getError()) + } + return Result.ok([...decrypted, ...result.getValue()]) + } + + if (!password) { + throw Error('Attempting to decrypt encrypted file with no password') + } + + const results = await this.handleEncryptedFileType({ + payloads: encrypted, + file, + password, + }) + + if (results.isFailed()) { + return Result.fail(results.getError()) + } + + return Result.ok([...decrypted, ...results.getValue()]) + } + + /** This is a backup file made from a session which had an encryption source, such as an account or a passcode. */ + private async handleEncryptedFileType(dto: { + file: BackupFile + password: string + payloads: EncryptedPayloadInterface[] + }): Promise> { + const keyParams = CreateAnyKeyParams((dto.file.keyParams || dto.file.auth_params) as AnyKeyParamsContent) + const rootKey = await this.encryption.computeRootKey(dto.password, keyParams) + + const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = [] + + const { rootKeyEncryption, itemsKeyEncryption, keySystemRootKeyEncryption } = SplitPayloadsByEncryptionType( + dto.payloads, + ) + + /** Decrypts items encrypted with a user root key, such as contacts, synced vault root keys, and items keys */ + const rootKeyBasedDecryptionResults = await this.encryption.decryptSplit({ + usesRootKey: { + items: rootKeyEncryption || [], + key: rootKey, + }, + }) + + /** Extract items keys and synced vault root keys from root key decryption results */ + const recentlyDecryptedKeys: (ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface)[] = + rootKeyBasedDecryptionResults + .filter((x) => isItemsKey(x) || isKeySystemRootKey(x)) + .filter(isDecryptedPayload) + .map((p) => CreateDecryptedItemFromPayload(p)) + + /** + * Now handle encrypted keySystemRootKeyEncryption items (vault items keys). For every encrypted vault items key + * find the respective vault root key, either from recently decrypted above, or from the key manager. Decrypt the + * vault items key, and if successful, add it to recentlyDecryptedKeys so that it can be used to decrypt subsequent items + */ + for (const payload of keySystemRootKeyEncryption ?? []) { + if (!payload.key_system_identifier) { + throw new Error('Attempting to decrypt key system root key encrypted payload with no key system identifier') + } + + const keys = rootKeyBasedDecryptionResults + .filter(isDecryptedPayload) + .filter(isKeySystemRootKey) + .map((p) => CreateDecryptedItemFromPayload(p)) as unknown as KeySystemRootKeyInterface[] + + const key = + keys.find((k) => k.systemIdentifier === payload.key_system_identifier) ?? + this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier) + + if (!key) { + results.push( + payload.copy({ + errorDecrypting: true, + }), + ) + continue + } + + const result = await this.encryption.decryptSplitSingle({ + usesKeySystemRootKey: { + items: [payload], + key, + }, + }) + + if (isDecryptedPayload(result) && isKeySystemItemsKey(result)) { + recentlyDecryptedKeys.push(CreateDecryptedItemFromPayload(result)) + } + + results.push(result) + } + + extendArray(results, rootKeyBasedDecryptionResults) + + const payloadsToDecrypt = [...(itemsKeyEncryption ?? [])] + + const decryptedPayloads = await this._decryptBackupPayloads.execute({ + payloads: payloadsToDecrypt, + recentlyDecryptedKeys: recentlyDecryptedKeys, + keyParams: keyParams, + rootKey: rootKey, + }) + if (decryptedPayloads.isFailed()) { + return Result.fail(decryptedPayloads.getError()) + } + + extendArray(results, decryptedPayloads.getValue()) + + return Result.ok(results) + } + + /** + * These are backup files made when not signed into an account and without an encryption source such as a passcode. + * In this case the items key exists in the backup in plaintext, but the items are encrypted with this items key. + */ + private async handleEncryptedWithNonEncryptedItemsKeyFileType( + payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[], + ): Promise> { + const decryptedItemsKeys: DecryptedPayloadInterface[] = [] + const encryptedPayloads: EncryptedPayloadInterface[] = [] + + payloads.forEach((payload) => { + if (payload.content_type === ContentType.TYPES.ItemsKey && isDecryptedPayload(payload)) { + decryptedItemsKeys.push(payload as DecryptedPayloadInterface) + } else if (isEncryptedPayload(payload)) { + encryptedPayloads.push(payload) + } + }) + + const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload(p)) + + return this._decryptBackupPayloads.execute({ + payloads: encryptedPayloads, + recentlyDecryptedKeys: itemsKeys, + rootKey: undefined, + }) + } + + private convertToPayloads(file: BackupFile): (EncryptedPayloadInterface | DecryptedPayloadInterface)[] { + return file.items.map((item) => { + if (isEncryptedTransferPayload(item)) { + return new EncryptedPayload(item) + } else if (isDecryptedTransferPayload(item)) { + return new DecryptedPayload(item) + } else { + throw Error('Unhandled case in DecryptBackupFile') + } + }) + } +} diff --git a/packages/services/src/Domain/Import/DecryptBackupPayloads.ts b/packages/services/src/Domain/Import/DecryptBackupPayloads.ts new file mode 100644 index 000000000..af762343d --- /dev/null +++ b/packages/services/src/Domain/Import/DecryptBackupPayloads.ts @@ -0,0 +1,91 @@ +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { + EncryptedPayloadInterface, + ItemsKeyInterface, + RootKeyInterface, + RootKeyParamsInterface, + DecryptedPayloadInterface, + isKeySystemRootKey, + KeySystemRootKeyInterface, + KeySystemItemsKeyInterface, +} from '@standardnotes/models' +import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { DetermineKeyToUse } from './DetermineKeyToUse' +import { isItemsKey, isKeySystemItemsKey } from '@standardnotes/encryption' +import { LoggerInterface } from '@standardnotes/utils' + +type EncryptedOrDecrypted = (DecryptedPayloadInterface | EncryptedPayloadInterface)[] + +export class DecryptBackupPayloads implements UseCaseInterface { + constructor( + private encryption: EncryptionProviderInterface, + private _determineKeyToUse: DetermineKeyToUse, + private logger: LoggerInterface, + ) {} + + async execute(dto: { + payloads: EncryptedPayloadInterface[] + recentlyDecryptedKeys: (ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface)[] + rootKey: RootKeyInterface | undefined + keyParams?: RootKeyParamsInterface + }): Promise> { + const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = [] + + for (const encryptedPayload of dto.payloads) { + try { + const key = this._determineKeyToUse + .execute({ + payload: encryptedPayload, + recentlyDecryptedKeys: dto.recentlyDecryptedKeys, + keyParams: dto.keyParams, + rootKey: dto.rootKey, + }) + .getValue() + + if (!key) { + results.push( + encryptedPayload.copy({ + errorDecrypting: true, + }), + ) + continue + } + + if (isItemsKey(key) || isKeySystemItemsKey(key)) { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesItemsKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } else if (isKeySystemRootKey(key)) { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesKeySystemRootKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } else { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesRootKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } + } catch (e) { + results.push( + encryptedPayload.copy({ + errorDecrypting: true, + }), + ) + this.logger.error('Error decrypting payload', encryptedPayload, e) + } + } + + return Result.ok(results) + } +} diff --git a/packages/services/src/Domain/Import/DetermineKeyToUse.ts b/packages/services/src/Domain/Import/DetermineKeyToUse.ts new file mode 100644 index 000000000..3bc1552fc --- /dev/null +++ b/packages/services/src/Domain/Import/DetermineKeyToUse.ts @@ -0,0 +1,108 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { compareVersions, leftVersionGreaterThanOrEqualToRight, ProtocolVersion } from '@standardnotes/common' +import { + ContentTypeUsesKeySystemRootKeyEncryption, + ContentTypeUsesRootKeyEncryption, + EncryptedPayloadInterface, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + RootKeyInterface, + KeySystemRootKeyInterface, + RootKeyParamsInterface, + isKeySystemRootKey, +} from '@standardnotes/models' +import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface' +import { isItemsKey } from '@standardnotes/encryption' + +type AnyKey = ItemsKeyInterface | RootKeyInterface | KeySystemRootKeyInterface | KeySystemItemsKeyInterface + +export class DetermineKeyToUse implements SyncUseCaseInterface { + constructor( + private encryption: EncryptionProviderInterface, + private keys: KeySystemKeyManagerInterface, + ) {} + + execute(dto: { + payload: EncryptedPayloadInterface + recentlyDecryptedKeys: (ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface)[] + keyParams?: RootKeyParamsInterface + rootKey?: RootKeyInterface + }): Result { + if (ContentTypeUsesRootKeyEncryption(dto.payload.content_type)) { + if (!dto.rootKey) { + throw new Error('Attempting to decrypt root key encrypted payload with no root key') + } + return Result.ok(dto.rootKey) + } + + if (ContentTypeUsesKeySystemRootKeyEncryption(dto.payload.content_type)) { + if (!dto.payload.key_system_identifier) { + throw new Error('Attempting to decrypt key system root key encrypted payload with no key system identifier') + } + try { + const recentlyDecrypted = dto.recentlyDecryptedKeys.filter(isKeySystemRootKey) + let keySystemRootKey = recentlyDecrypted.find( + (key) => key.systemIdentifier === dto.payload.key_system_identifier, + ) + if (!keySystemRootKey) { + keySystemRootKey = this.keys.getPrimaryKeySystemRootKey(dto.payload.key_system_identifier) + } + + return Result.ok(keySystemRootKey) + } catch (error) { + return Result.fail(JSON.stringify(error)) + } + } + + if (dto.payload.key_system_identifier) { + const keySystemItemsKey: KeySystemItemsKeyInterface | undefined = dto.recentlyDecryptedKeys.find( + (key) => key.key_system_identifier === dto.payload.key_system_identifier, + ) as KeySystemItemsKeyInterface | undefined + + if (keySystemItemsKey) { + return Result.ok(keySystemItemsKey) + } + } + + let itemsKey: ItemsKeyInterface | RootKeyInterface | KeySystemItemsKeyInterface | undefined + + if (dto.payload.items_key_id) { + itemsKey = this.encryption.itemsKeyForEncryptedPayload(dto.payload) + if (itemsKey) { + return Result.ok(itemsKey) + } + } + + itemsKey = dto.recentlyDecryptedKeys.filter(isItemsKey).find((itemsKeyPayload) => { + return Result.ok(dto.payload.items_key_id === itemsKeyPayload.uuid) + }) + + if (itemsKey) { + return Result.ok(itemsKey) + } + + if (!dto.keyParams) { + return Result.ok(undefined) + } + + const payloadVersion = dto.payload.version as ProtocolVersion + + /** + * Payloads with versions <= 003 use root key directly for encryption. + * However, if the incoming key params are >= 004, this means we should + * have an items key based off the 003 root key. We can't use the 004 + * root key directly because it's missing dataAuthenticationKey. + */ + if (leftVersionGreaterThanOrEqualToRight(dto.keyParams.version, ProtocolVersion.V004)) { + itemsKey = this.encryption.defaultItemsKeyForItemVersion( + payloadVersion, + dto.recentlyDecryptedKeys.filter(isItemsKey), + ) + } else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) { + itemsKey = dto.rootKey + } + + return Result.ok(itemsKey) + } +} diff --git a/packages/services/src/Domain/Import/GetBackupFileType.ts b/packages/services/src/Domain/Import/GetBackupFileType.ts new file mode 100644 index 000000000..57ffa0b21 --- /dev/null +++ b/packages/services/src/Domain/Import/GetBackupFileType.ts @@ -0,0 +1,24 @@ +import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { BackupFileType } from '@standardnotes/encryption' +import { BackupFile, PayloadInterface, isDecryptedPayload, isEncryptedPayload } from '@standardnotes/models' + +export class GetBackupFileType implements SyncUseCaseInterface { + execute(file: BackupFile, payloads: PayloadInterface[]): Result { + if (file.keyParams || file.auth_params) { + return Result.ok(BackupFileType.Encrypted) + } + + const hasEncryptedItem = payloads.find(isEncryptedPayload) + const hasDecryptedItemsKey = payloads.find( + (payload) => payload.content_type === ContentType.TYPES.ItemsKey && isDecryptedPayload(payload), + ) + + if (hasEncryptedItem && hasDecryptedItemsKey) { + return Result.ok(BackupFileType.EncryptedWithNonEncryptedItemsKey) + } else if (!hasEncryptedItem) { + return Result.ok(BackupFileType.FullyDecrypted) + } else { + return Result.ok(BackupFileType.Corrupt) + } + } +} diff --git a/packages/services/src/Domain/Import/GetFilePassword.ts b/packages/services/src/Domain/Import/GetFilePassword.ts new file mode 100644 index 000000000..ec0da5888 --- /dev/null +++ b/packages/services/src/Domain/Import/GetFilePassword.ts @@ -0,0 +1,29 @@ +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { + Challenge, + ChallengePrompt, + ChallengeReason, + ChallengeServiceInterface, + ChallengeValidation, +} from '../Challenge' +import { Strings } from './Strings' + +export class GetFilePassword implements UseCaseInterface { + constructor(private challenges: ChallengeServiceInterface) {} + + async execute(): Promise> { + const challenge = new Challenge( + [new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)], + ChallengeReason.DecryptEncryptedFile, + true, + ) + + const passwordResponse = await this.challenges.promptForChallengeResponse(challenge) + if (passwordResponse == undefined) { + return Result.fail('Import aborted due to canceled password prompt') + } + + this.challenges.completeChallenge(challenge) + return Result.ok(passwordResponse?.values[0].value as string) + } +} diff --git a/packages/services/src/Domain/Import/ImportData.ts b/packages/services/src/Domain/Import/ImportData.ts new file mode 100644 index 000000000..b88bcb60e --- /dev/null +++ b/packages/services/src/Domain/Import/ImportData.ts @@ -0,0 +1,145 @@ +import { DecryptBackupFile } from './DecryptBackupFile' +import { HistoryServiceInterface } from '../History/HistoryServiceInterface' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { ProtocolVersion, compareVersions } from '@standardnotes/common' +import { + BackupFile, + BackupFileDecryptedContextualPayload, + CreateDecryptedBackupFileContextPayload, + CreateEncryptedBackupFileContextPayload, + DecryptedItemInterface, + DecryptedPayloadInterface, + EncryptedPayloadInterface, + isDecryptedPayload, + isEncryptedPayload, + isEncryptedTransferPayload, +} from '@standardnotes/models' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { Strings } from './Strings' +import { ImportDataResult } from './ImportDataResult' +import { GetFilePassword } from './GetFilePassword' + +export class ImportData implements UseCaseInterface { + constructor( + private items: ItemManagerInterface, + private sync: SyncServiceInterface, + private protections: ProtectionsClientInterface, + private encryption: EncryptionProviderInterface, + private payloads: PayloadManagerInterface, + private history: HistoryServiceInterface, + private _decryptBackFile: DecryptBackupFile, + private _getFilePassword: GetFilePassword, + ) {} + + async execute(data: BackupFile, awaitSync = false): Promise> { + const versionValidation = this.validateFileVersion(data) + if (versionValidation.isFailed()) { + return Result.fail(versionValidation.getError()) + } + + const decryptedPayloadsOrError = await this.decryptData(data) + if (decryptedPayloadsOrError.isFailed()) { + return Result.fail(decryptedPayloadsOrError.getError()) + } + + const valid = this.getValidPayloadsToImportFromDecryptedResult(decryptedPayloadsOrError.getValue()) + + if (!(await this.protections.authorizeFileImport())) { + return Result.fail('Import aborted') + } + + const affectedUuids = await this.payloads.importPayloads(valid, this.history.getHistoryMapCopy()) + + const promise = this.sync.sync() + if (awaitSync) { + await promise + } + + const affectedItems = this.items.findItems(affectedUuids) as DecryptedItemInterface[] + + return Result.ok({ + affectedItems: affectedItems, + errorCount: decryptedPayloadsOrError.getValue().length - valid.length, + }) + } + + private validateFileVersion(data: BackupFile): Result { + if (data.version) { + const result = this.validateVersion(data.version) + if (result.isFailed()) { + return Result.fail(result.getError()) + } + } + return Result.ok() + } + + private async decryptData( + data: BackupFile, + ): Promise> { + let password: string | undefined + + if (data.auth_params || data.keyParams) { + const passwordResult = await this._getFilePassword.execute() + if (passwordResult.isFailed()) { + return Result.fail(passwordResult.getError()) + } + password = passwordResult.getValue() + } + + this.cleanImportData(data) + + const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password) + if (decryptedPayloadsOrError.isFailed()) { + return Result.fail(decryptedPayloadsOrError.getError()) + } + + return Result.ok(decryptedPayloadsOrError.getValue()) + } + + private getValidPayloadsToImportFromDecryptedResult( + results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[], + ): (EncryptedPayloadInterface | DecryptedPayloadInterface)[] { + const decrypted = results.filter(isDecryptedPayload) + const encrypted = results.filter(isEncryptedPayload) + const vaulted = encrypted.filter((payload) => { + return payload.key_system_identifier !== undefined + }) + + const valid = [...decrypted, ...vaulted] + return valid + } + + private cleanImportData(data: BackupFile): void { + data.items = data.items.map((item) => { + if (isEncryptedTransferPayload(item)) { + return CreateEncryptedBackupFileContextPayload(item) + } else { + return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload) + } + }) + } + + /** + * Prior to 003 backup files did not have a version field so we cannot + * stop importing if there is no backup file version, only if there is + * an unsupported version. + */ + private validateVersion(version: ProtocolVersion): Result { + const supportedVersions = this.encryption.supportedVersions() + if (!supportedVersions.includes(version)) { + return Result.fail(Strings.UnsupportedBackupFileVersion) + } + + const userVersion = this.encryption.getUserVersion() + if (userVersion && compareVersions(version, userVersion) === 1) { + /** File was made with a greater version than the user's account */ + return Result.fail(Strings.BackupFileMoreRecentThanAccount) + } + + return Result.ok() + } +} diff --git a/packages/services/src/Domain/Import/ImportDataResult.ts b/packages/services/src/Domain/Import/ImportDataResult.ts new file mode 100644 index 000000000..e96ca9c60 --- /dev/null +++ b/packages/services/src/Domain/Import/ImportDataResult.ts @@ -0,0 +1,9 @@ +import { DecryptedItemInterface } from '@standardnotes/models' + +export type ImportDataResult = { + // Items that were either created or dirtied by this import + affectedItems: DecryptedItemInterface[] + + // The number of items that were not imported due to failure to decrypt. + errorCount: number +} diff --git a/packages/services/src/Domain/Import/Strings.ts b/packages/services/src/Domain/Import/Strings.ts new file mode 100644 index 000000000..cabc445ed --- /dev/null +++ b/packages/services/src/Domain/Import/Strings.ts @@ -0,0 +1,7 @@ +export const Strings = { + UnsupportedBackupFileVersion: + 'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.', + BackupFileMoreRecentThanAccount: + "This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.", + FileAccountPassword: 'File account password', +} diff --git a/packages/services/src/Domain/Mutator/ImportDataUseCase.ts b/packages/services/src/Domain/Mutator/ImportDataUseCase.ts deleted file mode 100644 index 88fb4dcbe..000000000 --- a/packages/services/src/Domain/Mutator/ImportDataUseCase.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { DecryptBackupFile } from '../Encryption/UseCase/DecryptBackupFile' -import { HistoryServiceInterface } from '../History/HistoryServiceInterface' -import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface' -import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' -import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface' -import { SyncServiceInterface } from '../Sync/SyncServiceInterface' -import { ItemManagerInterface } from '../Item/ItemManagerInterface' -import { ProtocolVersion, compareVersions } from '@standardnotes/common' -import { - BackupFile, - BackupFileDecryptedContextualPayload, - CreateDecryptedBackupFileContextPayload, - CreateEncryptedBackupFileContextPayload, - DecryptedItemInterface, - isDecryptedPayload, - isEncryptedPayload, - isEncryptedTransferPayload, -} from '@standardnotes/models' -import { ClientDisplayableError } from '@standardnotes/responses' -import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge' -import { Result } from '@standardnotes/domain-core' -import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' - -const Strings = { - UnsupportedBackupFileVersion: - 'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.', - BackupFileMoreRecentThanAccount: - "This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.", - FileAccountPassword: 'File account password', -} - -export type ImportDataReturnType = - | { - affectedItems: DecryptedItemInterface[] - errorCount: number - } - | { - error: ClientDisplayableError - } - -export class ImportDataUseCase { - constructor( - private itemManager: ItemManagerInterface, - private sync: SyncServiceInterface, - private protectionService: ProtectionsClientInterface, - private encryption: EncryptionProviderInterface, - private payloadManager: PayloadManagerInterface, - private challengeService: ChallengeServiceInterface, - private historyService: HistoryServiceInterface, - private _decryptBackFile: DecryptBackupFile, - ) {} - - /** - * @returns - * .affectedItems: Items that were either created or dirtied by this import - * .errorCount: The number of items that were not imported due to failure to decrypt. - */ - async execute(data: BackupFile, awaitSync = false): Promise { - if (data.version) { - const result = this.validateVersion(data.version) - if (result.isFailed()) { - return { error: new ClientDisplayableError(result.getError()) } - } - } - - let password: string | undefined - - if (data.auth_params || data.keyParams) { - const passwordResult = await this.getFilePassword() - if (passwordResult.isFailed()) { - return { error: new ClientDisplayableError(passwordResult.getError()) } - } - password = passwordResult.getValue() - } - - if (!(await this.protectionService.authorizeFileImport())) { - return { error: new ClientDisplayableError('Import aborted') } - } - - data.items = data.items.map((item) => { - if (isEncryptedTransferPayload(item)) { - return CreateEncryptedBackupFileContextPayload(item) - } else { - return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload) - } - }) - - const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password) - if (decryptedPayloadsOrError instanceof ClientDisplayableError) { - return { error: decryptedPayloadsOrError } - } - - const decryptedPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload) - const encryptedPayloads = decryptedPayloadsOrError.filter(isEncryptedPayload) - const acceptableEncryptedPayloads = encryptedPayloads.filter((payload) => { - return payload.key_system_identifier !== undefined - }) - const importablePayloads = [...decryptedPayloads, ...acceptableEncryptedPayloads] - - const affectedUuids = await this.payloadManager.importPayloads( - importablePayloads, - this.historyService.getHistoryMapCopy(), - ) - - const promise = this.sync.sync() - if (awaitSync) { - await promise - } - - const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[] - - return { - affectedItems: affectedItems, - errorCount: decryptedPayloadsOrError.length - importablePayloads.length, - } - } - - private async getFilePassword(): Promise> { - const challenge = new Challenge( - [new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)], - ChallengeReason.DecryptEncryptedFile, - true, - ) - const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge) - if (passwordResponse == undefined) { - /** Challenge was canceled */ - return Result.fail('Import aborted') - } - this.challengeService.completeChallenge(challenge) - return Result.ok(passwordResponse?.values[0].value as string) - } - - /** - * Prior to 003 backup files did not have a version field so we cannot - * stop importing if there is no backup file version, only if there is - * an unsupported version. - */ - private validateVersion(version: ProtocolVersion): Result { - const supportedVersions = this.encryption.supportedVersions() - if (!supportedVersions.includes(version)) { - return Result.fail(Strings.UnsupportedBackupFileVersion) - } - - const userVersion = this.encryption.getUserVersion() - if (userVersion && compareVersions(version, userVersion) === 1) { - /** File was made with a greater version than the user's account */ - return Result.fail(Strings.BackupFileMoreRecentThanAccount) - } - - return Result.ok() - } -} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 63249ab6b..dd61dc282 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -70,7 +70,6 @@ export * from './Encryption/UseCase/Asymmetric/DecryptMessage' export * from './Encryption/UseCase/Asymmetric/DecryptOwnMessage' export * from './Encryption/UseCase/Asymmetric/EncryptMessage' export * from './Encryption/UseCase/Asymmetric/GetMessageAdditionalData' -export * from './Encryption/UseCase/DecryptBackupFile' export * from './Encryption/UseCase/DecryptErroredPayloads' export * from './Encryption/UseCase/GetKeyPairs' export * from './Encryption/UseCase/ItemsKey/CreateNewDefaultItemsKey' @@ -101,6 +100,15 @@ export * from './HomeServer/HomeServerManagerInterface' export * from './HomeServer/HomeServerService' export * from './HomeServer/HomeServerServiceInterface' export * from './HomeServer/HomeServerStatus' +export * from './Import/DecryptBackupFile' +export * from './Import/DecryptBackupPayloads' +export * from './Import/DetermineKeyToUse' +export * from './Import/GetBackupFileType' +export * from './Import/GetFilePassword' +export * from './Import/ImportData' +export * from './Import/ImportDataResult' +export * from './Import/CreateDecryptedBackupFile' +export * from './Import/CreateEncryptedBackupFile' export * from './Integrity/IntegrityApiInterface' export * from './Integrity/IntegrityEvent' export * from './Integrity/IntegrityEventPayload' @@ -122,7 +130,6 @@ export * from './ItemsEncryption/ItemsEncryption' export * from './ItemsEncryption/ItemsEncryption' export * from './KeySystem/KeySystemKeyManager' export * from './Mfa/MfaServiceInterface' -export * from './Mutator/ImportDataUseCase' export * from './Mutator/MutatorClientInterface' export * from './Payloads/PayloadManagerInterface' export * from './Preferences/PreferenceServiceInterface' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 944487a53..205b1e464 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -20,8 +20,8 @@ import { ApplicationStageChangedEventPayload, StorageValueModes, ChallengeObserver, - ImportDataReturnType, - ImportDataUseCase, + ImportDataResult, + ImportData, StoragePersistencePolicies, HomeServerServiceInterface, DeviceInterface, @@ -79,6 +79,8 @@ import { SetHost, MfaServiceInterface, GenerateUuid, + CreateDecryptedBackupFile, + CreateEncryptedBackupFile, } from '@standardnotes/services' import { SNNote, @@ -133,6 +135,7 @@ import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAu import { Dependencies } from './Dependencies/Dependencies' import { TYPES } from './Dependencies/Types' import { RegisterApplicationServicesEvents } from './Dependencies/DependencyEvents' +import { Result } from '@standardnotes/domain-core' /** How often to automatically sync, in milliseconds */ const DEFAULT_AUTO_SYNC_INTERVAL = 30_000 @@ -672,26 +675,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.protections.authorizeAutolockIntervalChange() } - public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise { - return this.encryption.createEncryptedBackupFile() - } - - public async createEncryptedBackupFile(): Promise { - if (!(await this.protections.authorizeBackupCreation())) { - return - } - - return this.encryption.createEncryptedBackupFile() - } - - public async createDecryptedBackupFile(): Promise { - if (!(await this.protections.authorizeBackupCreation())) { - return - } - - return this.encryption.createDecryptedBackupFile() - } - public isEphemeralSession(): boolean { return this.storage.isEphemeralSession() } @@ -842,8 +825,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli }) } - public async importData(data: BackupFile, awaitSync = false): Promise { - const usecase = this.dependencies.get(TYPES.ImportDataUseCase) + public async importData(data: BackupFile, awaitSync = false): Promise> { + const usecase = this.dependencies.get(TYPES.ImportData) return usecase.execute(data, awaitSync) } @@ -1164,6 +1147,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.dependencies.get(TYPES.GenerateUuid) } + public get createDecryptedBackupFile(): CreateDecryptedBackupFile { + return this.dependencies.get(TYPES.CreateDecryptedBackupFile) + } + + public get createEncryptedBackupFile(): CreateEncryptedBackupFile { + return this.dependencies.get(TYPES.CreateEncryptedBackupFile) + } + private get migrations(): MigrationService { return this.dependencies.get(TYPES.MigrationService) } diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index 730f2fe54..810da71b6 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -42,7 +42,7 @@ import { GetAllContacts, GetVault, HomeServerService, - ImportDataUseCase, + ImportData, InMemoryStore, IntegrityService, InternalEventBus, @@ -133,7 +133,13 @@ import { GenerateUuid, GetVaultItems, ValidateVaultPassword, + DecryptBackupPayloads, + DetermineKeyToUse, + GetBackupFileType, + GetFilePassword, IsApplicationUsingThirdPartyHost, + CreateDecryptedBackupFile, + CreateEncryptedBackupFile, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -160,7 +166,7 @@ import { WebSocketServer, } from '@standardnotes/api' import { TYPES } from './Types' -import { Logger, isNotUndefined, isDeinitable } from '@standardnotes/utils' +import { Logger, isNotUndefined, isDeinitable, LoggerInterface } from '@standardnotes/utils' import { EncryptionOperators } from '@standardnotes/encryption' import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' @@ -219,6 +225,29 @@ export class Dependencies { } private registerUseCaseMakers() { + this.factory.set(TYPES.DecryptBackupPayloads, () => { + return new DecryptBackupPayloads( + this.get(TYPES.EncryptionService), + this.get(TYPES.DetermineKeyToUse), + this.get(TYPES.Logger), + ) + }) + + this.factory.set(TYPES.DetermineKeyToUse, () => { + return new DetermineKeyToUse( + this.get(TYPES.EncryptionService), + this.get(TYPES.KeySystemKeyManager), + ) + }) + + this.factory.set(TYPES.GetBackupFileType, () => { + return new GetBackupFileType() + }) + + this.factory.set(TYPES.GetFilePassword, () => { + return new GetFilePassword(this.get(TYPES.ChallengeService)) + }) + this.factory.set(TYPES.ValidateVaultPassword, () => { return new ValidateVaultPassword( this.get(TYPES.EncryptionService), @@ -273,16 +302,31 @@ export class Dependencies { ) }) - this.factory.set(TYPES.ImportDataUseCase, () => { - return new ImportDataUseCase( + this.factory.set(TYPES.CreateDecryptedBackupFile, () => { + return new CreateDecryptedBackupFile( + this.get(TYPES.PayloadManager), + this.get(TYPES.ProtectionService), + ) + }) + + this.factory.set(TYPES.CreateEncryptedBackupFile, () => { + return new CreateEncryptedBackupFile( + this.get(TYPES.ItemManager), + this.get(TYPES.ProtectionService), + this.get(TYPES.EncryptionService), + ) + }) + + this.factory.set(TYPES.ImportData, () => { + return new ImportData( this.get(TYPES.ItemManager), this.get(TYPES.SyncService), this.get(TYPES.ProtectionService), this.get(TYPES.EncryptionService), this.get(TYPES.PayloadManager), - this.get(TYPES.ChallengeService), this.get(TYPES.HistoryManager), this.get(TYPES.DecryptBackupFile), + this.get(TYPES.GetFilePassword), ) }) @@ -291,7 +335,12 @@ export class Dependencies { }) this.factory.set(TYPES.DecryptBackupFile, () => { - return new DecryptBackupFile(this.get(TYPES.EncryptionService), this.get(TYPES.Logger)) + return new DecryptBackupFile( + this.get(TYPES.EncryptionService), + this.get(TYPES.KeySystemKeyManager), + this.get(TYPES.GetBackupFileType), + this.get(TYPES.DecryptBackupPayloads), + ) }) this.factory.set(TYPES.DiscardItemsLocally, () => { diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 60e4c7a1c..5db17ce70 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -89,7 +89,7 @@ export const TYPES = { ListRevisions: Symbol.for('ListRevisions'), GetRevision: Symbol.for('GetRevision'), DeleteRevision: Symbol.for('DeleteRevision'), - ImportDataUseCase: Symbol.for('ImportDataUseCase'), + ImportData: Symbol.for('ImportData'), DiscardItemsLocally: Symbol.for('DiscardItemsLocally'), FindContact: Symbol.for('FindContact'), GetAllContacts: Symbol.for('GetAllContacts'), @@ -163,7 +163,13 @@ export const TYPES = { GenerateUuid: Symbol.for('GenerateUuid'), GetVaultItems: Symbol.for('GetVaultItems'), ValidateVaultPassword: Symbol.for('ValidateVaultPassword'), + DecryptBackupPayloads: Symbol.for('DecryptBackupPayloads'), + DetermineKeyToUse: Symbol.for('DetermineKeyToUse'), + GetBackupFileType: Symbol.for('GetBackupFileType'), + GetFilePassword: Symbol.for('GetFilePassword'), AuthorizeVaultDeletion: Symbol.for('AuthorizeVaultDeletion'), + CreateDecryptedBackupFile: Symbol.for('CreateDecryptedBackupFile'), + CreateEncryptedBackupFile: Symbol.for('CreateEncryptedBackupFile'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/mocha/backups.test.js b/packages/snjs/mocha/backups.test.js index 709c83bee..91a842353 100644 --- a/packages/snjs/mocha/backups.test.js +++ b/packages/snjs/mocha/backups.test.js @@ -23,16 +23,16 @@ describe('backups', function () { }) it('backup file should have a version number', async function () { - let data = await application.createDecryptedBackupFile() + let data = (await application.createDecryptedBackupFile.execute()).getValue() expect(data.version).to.equal(application.encryption.getLatestVersion()) await application.addPasscode('passcode') - data = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + data = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(data.version).to.equal(application.encryption.getLatestVersion()) }) it('no passcode + no account backup file should have correct number of items', async function () { await Promise.all([Factory.createSyncedNote(application), Factory.createSyncedNote(application)]) - const data = await application.createDecryptedBackupFile() + const data = (await application.createDecryptedBackupFile.execute()).getValue() const offsetForNewItems = 2 const offsetForNoItemsKey = -1 expect(data.items.length).to.equal(BaseItemCounts.DefaultItems + offsetForNewItems + offsetForNoItemsKey) @@ -44,12 +44,12 @@ describe('backups', function () { await Promise.all([Factory.createSyncedNote(application), Factory.createSyncedNote(application)]) // Encrypted backup without authorization - const encryptedData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const encryptedData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2) // Encrypted backup with authorization Factory.handlePasswordChallenges(application, passcode) - const authorizedEncryptedData = await application.createEncryptedBackupFile() + const authorizedEncryptedData = (await application.createEncryptedBackupFile.execute()).getValue() expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2) }) @@ -63,17 +63,17 @@ describe('backups', function () { await Promise.all([Factory.createSyncedNote(application), Factory.createSyncedNote(application)]) // Encrypted backup without authorization - const encryptedData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const encryptedData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) Factory.handlePasswordChallenges(application, password) // Decrypted backup - const decryptedData = await application.createDecryptedBackupFile() + const decryptedData = (await application.createDecryptedBackupFile.execute()).getValue() expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2) // Encrypted backup with authorization - const authorizedEncryptedData = await application.createEncryptedBackupFile() + const authorizedEncryptedData = (await application.createEncryptedBackupFile.execute()).getValue() expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) }) @@ -85,23 +85,25 @@ describe('backups', function () { await Promise.all([Factory.createSyncedNote(application), Factory.createSyncedNote(application)]) // Encrypted backup without authorization - const encryptedData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const encryptedData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) Factory.handlePasswordChallenges(application, passcode) // Decrypted backup - const decryptedData = await application.createDecryptedBackupFile() + const decryptedData = (await application.createDecryptedBackupFile.execute()).getValue() expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2) // Encrypted backup with authorization - const authorizedEncryptedData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const authorizedEncryptedData = ( + await application.createEncryptedBackupFile.execute({ skipAuthorization: true }) + ).getValue() expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) }).timeout(10000) it('backup file item should have correct fields', async function () { await Factory.createSyncedNote(application) - let backupData = await application.createDecryptedBackupFile() + let backupData = (await application.createDecryptedBackupFile.execute()).getValue() let rawItem = backupData.items.find((i) => i.content_type === ContentType.TYPES.Note) expect(rawItem.fields).to.not.be.ok @@ -120,7 +122,7 @@ describe('backups', function () { password: password, }) - backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() rawItem = backupData.items.find((i) => i.content_type === ContentType.TYPES.Note) expect(rawItem.fields).to.not.be.ok @@ -155,13 +157,13 @@ describe('backups', function () { expect(erroredItem.errorDecrypting).to.equal(true) - const backupData = await application.createDecryptedBackupFile() + const backupData = (await application.createDecryptedBackupFile.execute()).getValue() expect(backupData.items.length).to.equal(BaseItemCounts.DefaultItemsNoAccounNoItemsKey + 2) }) it('decrypted backup file should not have keyParams', async function () { - const backup = await application.createDecryptedBackupFile() + const backup = (await application.createDecryptedBackupFile.execute()).getValue() expect(backup).to.not.haveOwnProperty('keyParams') }) @@ -176,7 +178,7 @@ describe('backups', function () { Factory.handlePasswordChallenges(application, password) - const backup = await application.createDecryptedBackupFile() + const backup = (await application.createDecryptedBackupFile.execute()).getValue() expect(backup).to.not.haveOwnProperty('keyParams') @@ -185,30 +187,30 @@ describe('backups', function () { it('encrypted backup file should have keyParams', async function () { await application.addPasscode('passcode') - const backup = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backup = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(backup).to.haveOwnProperty('keyParams') }) it('decrypted backup file should not have itemsKeys', async function () { - const backup = await application.createDecryptedBackupFile() + const backup = (await application.createDecryptedBackupFile.execute()).getValue() expect(backup.items.some((item) => item.content_type === ContentType.TYPES.ItemsKey)).to.be.false }) it('encrypted backup file should have itemsKeys', async function () { await application.addPasscode('passcode') - const backup = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backup = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() expect(backup.items.some((item) => item.content_type === ContentType.TYPES.ItemsKey)).to.be.true }) it('backup file with no account and no passcode should be decrypted', async function () { const note = await Factory.createSyncedNote(application) - const backup = await application.createDecryptedBackupFile() + const backup = (await application.createDecryptedBackupFile.execute()).getValue() expect(backup).to.not.haveOwnProperty('keyParams') expect(backup.items.some((item) => item.content_type === ContentType.TYPES.ItemsKey)).to.be.false expect(backup.items.find((item) => item.content_type === ContentType.TYPES.Note).uuid).to.equal(note.uuid) let error try { - await application.createEncryptedBackupFileForAutomatedDesktopBackups() + ;(await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() } catch (e) { error = e } diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index f94585a9d..622690df6 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -43,7 +43,7 @@ describe('importing', function () { version: '-1', items: [], }) - expect(result.error).to.exist + expect(result.isFailed()).to.be.true }) it('should not import backups made from 004 into 003 account', async function () { @@ -57,7 +57,7 @@ describe('importing', function () { version: ProtocolVersion.V004, items: [], }) - expect(result.error).to.exist + expect(result.isFailed()).to.be.true }) it('importing existing data should keep relationships valid', async function () { @@ -361,7 +361,7 @@ describe('importing', function () { Factory.createMappedTag(application), ]) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await application.sync.sync({ awaitAll: true }) @@ -394,7 +394,7 @@ describe('importing', function () { Factory.createMappedTag(application), ]) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -421,7 +421,7 @@ describe('importing', function () { Factory.createMappedTag(application), ]) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -449,14 +449,13 @@ describe('importing', function () { text: 'On protocol version 003.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) @@ -478,14 +477,13 @@ describe('importing', function () { text: 'On protocol version 004.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) @@ -507,7 +505,7 @@ describe('importing', function () { text: 'On protocol version 004.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -523,8 +521,7 @@ describe('importing', function () { backupData.items = [...backupData.items, madeUpPayload] - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(backupData.items.length - 1) expect(result.errorCount).to.be.eq(1) }) @@ -543,7 +540,7 @@ describe('importing', function () { text: 'On protocol version 003.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -559,8 +556,7 @@ describe('importing', function () { }, }) - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(0) expect(result.errorCount).to.be.eq(backupData.items.length) @@ -579,7 +575,7 @@ describe('importing', function () { text: 'On protocol version 004.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -590,8 +586,7 @@ describe('importing', function () { }, }) - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(0) expect(result.errorCount).to.be.eq(backupData.items.length) expect(application.items.getDisplayableNotes().length).to.equal(0) @@ -609,7 +604,7 @@ describe('importing', function () { text: 'On protocol version 004.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() delete backupData.keyParams await Factory.safeDeinit(application) @@ -617,7 +612,7 @@ describe('importing', function () { const result = await application.importData(backupData) - expect(result.error).to.be.ok + expect(result.isFailed()).to.be.true }) it('should not import payloads if the corresponding ItemsKey is not present within the backup file', async function () { @@ -633,16 +628,14 @@ describe('importing', function () { text: 'On protocol version 004.', }) - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() backupData.items = backupData.items.filter((payload) => payload.content_type !== ContentType.TYPES.ItemsKey) await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.importData(backupData, true) - - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.equal(BaseItemCounts.BackupFileRootKeyEncryptedItems) @@ -664,7 +657,7 @@ describe('importing', function () { await application.sync.sync() - const backupData = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + const backupData = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() @@ -739,8 +732,7 @@ describe('importing', function () { Factory.handlePasswordChallenges(application, 'password') - const result = await application.importData(backupData, true) - expect(result).to.not.be.undefined + const result = (await application.importData(backupData, true)).getValue() expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) }) @@ -867,7 +859,7 @@ describe('importing', function () { }, } - const result = await application.importData(backupFile, false) + const result = (await application.importData(backupFile, false)).getValue() expect(result.errorCount).to.equal(0) await Factory.safeDeinit(application) }) diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index 0a4dc5dd9..c9dd77bf8 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -778,7 +778,7 @@ describe('online conflict handling', function () { it('importing data belonging to another account should not result in duplication', async () => { /** Create primary account and export data */ await createSyncedNoteWithTag(application) - let backupFile = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + let backupFile = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() /** Sort matters, and is the cause of the original issue, where tag comes before the note */ backupFile.items = [ backupFile.items.find((i) => i.content_type === ContentType.TYPES.ItemsKey), @@ -813,7 +813,7 @@ describe('online conflict handling', function () { await application.changeAndSaveItem.execute(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note2) }) - let backupFile = await application.createEncryptedBackupFileForAutomatedDesktopBackups() + let backupFile = (await application.createEncryptedBackupFile.execute({ skipAuthorization: true })).getValue() backupFile.items = [ backupFile.items.find((i) => i.content_type === ContentType.TYPES.ItemsKey), backupFile.items.filter((i) => i.content_type === ContentType.TYPES.Note)[0], diff --git a/packages/snjs/mocha/vaults/asymmetric-messages.test.js b/packages/snjs/mocha/vaults/asymmetric-messages.test.js index 26c345b2a..7fa7ea6b1 100644 --- a/packages/snjs/mocha/vaults/asymmetric-messages.test.js +++ b/packages/snjs/mocha/vaults/asymmetric-messages.test.js @@ -22,6 +22,7 @@ describe('asymmetric messages', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => { diff --git a/packages/snjs/mocha/vaults/conflicts.test.js b/packages/snjs/mocha/vaults/conflicts.test.js index 39d8879ac..33100b506 100644 --- a/packages/snjs/mocha/vaults/conflicts.test.js +++ b/packages/snjs/mocha/vaults/conflicts.test.js @@ -22,6 +22,7 @@ describe('shared vault conflicts', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('after being removed from shared vault, attempting to sync previous vault item should result in SharedVaultNotMemberError. The item should be duplicated then removed.', async () => { diff --git a/packages/snjs/mocha/vaults/contacts.test.js b/packages/snjs/mocha/vaults/contacts.test.js index 7ba28f2a3..42b1a3377 100644 --- a/packages/snjs/mocha/vaults/contacts.test.js +++ b/packages/snjs/mocha/vaults/contacts.test.js @@ -22,6 +22,7 @@ describe('contacts', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should create contact', async () => { diff --git a/packages/snjs/mocha/vaults/crypto.test.js b/packages/snjs/mocha/vaults/crypto.test.js index 1275d8953..6aaf74fc1 100644 --- a/packages/snjs/mocha/vaults/crypto.test.js +++ b/packages/snjs/mocha/vaults/crypto.test.js @@ -22,6 +22,7 @@ describe('shared vault crypto', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) describe('root key', () => { diff --git a/packages/snjs/mocha/vaults/deletion.test.js b/packages/snjs/mocha/vaults/deletion.test.js index 1539dc979..63fd9b700 100644 --- a/packages/snjs/mocha/vaults/deletion.test.js +++ b/packages/snjs/mocha/vaults/deletion.test.js @@ -8,7 +8,6 @@ describe('shared vault deletion', function () { this.timeout(Factory.TwentySecondTimeout) let context - let sharedVaults beforeEach(async function () { localStorage.clear() @@ -17,14 +16,13 @@ describe('shared vault deletion', function () { await context.launch() await context.register() - - sharedVaults = context.sharedVaults }) afterEach(async function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should remove item from all user devices when item is deleted permanently', async () => { @@ -72,7 +70,7 @@ describe('shared vault deletion', function () { const { sharedVault, note, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) - await sharedVaults.deleteSharedVault(sharedVault) + await context.sharedVaults.deleteSharedVault(sharedVault) await contactContext.sync() const originatorNote = context.items.findItem(note.uuid) diff --git a/packages/snjs/mocha/vaults/files.test.js b/packages/snjs/mocha/vaults/files.test.js index d29658e90..3e92f1f81 100644 --- a/packages/snjs/mocha/vaults/files.test.js +++ b/packages/snjs/mocha/vaults/files.test.js @@ -9,7 +9,6 @@ describe('shared vault files', function () { this.timeout(Factory.TwentySecondTimeout) let context - let vaults beforeEach(async function () { localStorage.clear() @@ -19,7 +18,6 @@ describe('shared vault files', function () { await context.launch() await context.register() - vaults = context.vaults await context.activatePaidSubscriptionForUser() }) @@ -27,6 +25,7 @@ describe('shared vault files', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) describe('private vaults', () => { @@ -68,7 +67,7 @@ describe('shared vault files', function () { const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000) const sharedVault = await Collaboration.createSharedVault(context) - const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile) + const addedFile = await context.vaults.moveItemToVault(sharedVault, uploadedFile) const downloadedBytes = await Files.downloadFile(context.files, addedFile) expect(downloadedBytes).to.eql(buffer) @@ -82,7 +81,7 @@ describe('shared vault files', function () { const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault) const secondVault = await Collaboration.createSharedVault(context) - const movedFile = await vaults.moveItemToVault(secondVault, uploadedFile) + const movedFile = await context.vaults.moveItemToVault(secondVault, uploadedFile) const downloadedBytes = await Files.downloadFile(context.files, movedFile) expect(downloadedBytes).to.eql(buffer) @@ -96,7 +95,7 @@ describe('shared vault files', function () { const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault) const privateVault = await Collaboration.createPrivateVault(context) - const addedFile = await vaults.moveItemToVault(privateVault, uploadedFile) + const addedFile = await context.vaults.moveItemToVault(privateVault, uploadedFile) const downloadedBytes = await Files.downloadFile(context.files, addedFile) expect(downloadedBytes).to.eql(buffer) @@ -112,14 +111,14 @@ describe('shared vault files', function () { const sharedVault = await Collaboration.createSharedVault(context) - vaults.alerts.confirmV2 = () => Promise.resolve(true) + context.vaults.alerts.confirmV2 = () => Promise.resolve(true) - await vaults.moveItemToVault(sharedVault, note) + await context.vaults.moveItemToVault(sharedVault, note) const latestFile = context.items.findItem(updatedFile.uuid) - expect(vaults.getItemVault(latestFile).uuid).to.equal(sharedVault.uuid) - expect(vaults.getItemVault(context.items.findItem(note.uuid)).uuid).to.equal(sharedVault.uuid) + expect(context.vaults.getItemVault(latestFile).uuid).to.equal(sharedVault.uuid) + expect(context.vaults.getItemVault(context.items.findItem(note.uuid)).uuid).to.equal(sharedVault.uuid) const downloadedBytes = await Files.downloadFile(context.files, latestFile) expect(downloadedBytes).to.eql(buffer) @@ -132,7 +131,7 @@ describe('shared vault files', function () { const sharedVault = await Collaboration.createSharedVault(context) const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) - const removedFile = await vaults.removeItemFromVault(uploadedFile) + const removedFile = await context.vaults.removeItemFromVault(uploadedFile) expect(removedFile.key_system_identifier).to.not.be.ok const downloadedBytes = await Files.downloadFile(context.files, removedFile) @@ -226,7 +225,7 @@ describe('shared vault files', function () { const response = await fetch('/mocha/assets/small_file.md') const buffer = new Uint8Array(await response.arrayBuffer()) const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000) - const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile) + const addedFile = await context.vaults.moveItemToVault(sharedVault, uploadedFile) await contactContext.sync() diff --git a/packages/snjs/mocha/vaults/importing.test.js b/packages/snjs/mocha/vaults/importing.test.js index 8b1a64d35..c325ee6f7 100644 --- a/packages/snjs/mocha/vaults/importing.test.js +++ b/packages/snjs/mocha/vaults/importing.test.js @@ -4,7 +4,7 @@ import * as Collaboration from '../lib/Collaboration.js' chai.use(chaiAsPromised) const expect = chai.expect -describe.skip('vault importing', function () { +describe('vault importing', function () { this.timeout(Factory.TwentySecondTimeout) let context @@ -15,45 +15,181 @@ describe.skip('vault importing', function () { context = await Factory.createVaultsContextWithRealCrypto() await context.launch() - await context.register() }) afterEach(async function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) - it('should import vaulted items with synced root key', async () => { - console.error('TODO: implement') + describe('exports', () => { + describe('no account and no passcode', () => { + it('should throw if attempting to create encrypted backup', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, vault, note) + + await context.application.vaultLocks.lockNonPersistentVault(vault) + + await Factory.expectThrowsAsync( + () => context.application.createEncryptedBackupFile.execute(), + 'Attempting root key encryption with no root key', + ) + }) + + it('decrypted backups should export unlocked password vaulted items as decrypted and locked as encrypted', async () => { + const lockedVault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + const lockedVaultNote = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, lockedVault, lockedVaultNote) + await context.application.vaultLocks.lockNonPersistentVault(lockedVault) + + const unlockedVault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + const unlockedVaultNote = await context.createSyncedNote('har', 'zar') + await Collaboration.moveItemToVault(context, unlockedVault, unlockedVaultNote) + + const backupData = (await context.application.createDecryptedBackupFile.execute()).getValue() + + const backupLockedVaultNote = backupData.items.find((item) => item.uuid === lockedVaultNote.uuid) + expect(isEncryptedPayload(backupLockedVaultNote)).to.be.true + + const backupUnlockedVaultNote = backupData.items.find((item) => item.uuid === unlockedVaultNote.uuid) + expect(isEncryptedPayload(backupUnlockedVaultNote)).to.be.false + }) + }) }) - it('should import vaulted items with non-present root key', async () => { - const vault = await context.vaults.createUserInputtedPasswordVault({ - name: 'test vault', - userInputtedPassword: 'test password', - storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + describe('imports', () => { + describe('password vaults', () => { + it('should import password vaulted items with non-present root key as-is without decrypting', async () => { + await context.register() + + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, vault, note) + + const backupData = ( + await context.application.createEncryptedBackupFile.execute({ skipAuthorization: true }) + ).getValue() + + const otherContext = await Factory.createVaultsContextWithRealCrypto() + otherContext.password = context.password + await otherContext.launch() + + await otherContext.application.importData(backupData) + + const expectedImportedItems = ['vault-items-key', 'note'] + const invalidItems = otherContext.items.invalidItems + expect(invalidItems.length).to.equal(expectedImportedItems.length) + + const encryptedItemsKey = invalidItems.find((item) => item.content_type === ContentType.TYPES.KeySystemItemsKey) + expect(encryptedItemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + expect(encryptedItemsKey.errorDecrypting).to.be.true + + const encryptedNote = invalidItems.find((item) => item.content_type === ContentType.TYPES.Note) + expect(encryptedNote.key_system_identifier).to.equal(vault.systemIdentifier) + expect(encryptedNote.errorDecrypting).to.be.true + expect(encryptedNote.uuid).to.equal(note.uuid) + + await otherContext.deinit() + }) }) - const note = await context.createSyncedNote('foo', 'bar') - await Collaboration.moveItemToVault(context, vault, note) + describe('randomized vaults', () => { + it('should import backup file for randomized vault created without account or passcode', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + }) - const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups() + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, vault, note) - const otherContext = await Factory.createVaultsContextWithRealCrypto() - await otherContext.launch() + const backupData = (await context.application.createDecryptedBackupFile.execute()).getValue() - await otherContext.application.importData(backupData) + const otherContext = await Factory.createVaultsContextWithRealCrypto() + otherContext.password = context.password + await otherContext.launch() - const expectedImportedItems = ['vault-items-key', 'note'] - const invalidItems = otherContext.items.invalidItems - expect(invalidItems.length).to.equal(expectedImportedItems.length) + const result = (await otherContext.application.importData(backupData)).getValue() - const encryptedItem = invalidItems[0] - expect(encryptedItem.key_system_identifier).to.equal(vault.systemIdentifier) - expect(encryptedItem.errorDecrypting).to.be.true - expect(encryptedItem.uuid).to.equal(note.uuid) + const invalidItems = otherContext.items.invalidItems + expect(invalidItems.length).to.equal(0) - await otherContext.deinit() + expect(result.affectedItems.length).to.equal(backupData.items.length) + + const itemsKey = result.affectedItems.find((item) => item.content_type === ContentType.TYPES.KeySystemItemsKey) + expect(itemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + + const importedNote = result.affectedItems.find((item) => item.content_type === ContentType.TYPES.Note) + expect(importedNote.key_system_identifier).to.equal(vault.systemIdentifier) + expect(importedNote.uuid).to.equal(note.uuid) + + const importedRootKey = result.affectedItems.find( + (item) => item.content_type === ContentType.TYPES.KeySystemRootKey, + ) + expect(importedRootKey.systemIdentifier).to.equal(vault.systemIdentifier) + + await otherContext.deinit() + }) + + it('should import synced-key vaulted items by decrypting', async () => { + await context.register() + + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + }) + + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, vault, note) + + const backupData = ( + await context.application.createEncryptedBackupFile.execute({ skipAuthorization: true }) + ).getValue() + + const otherContext = await Factory.createVaultsContextWithRealCrypto() + otherContext.password = context.password + await otherContext.launch() + + const result = (await otherContext.application.importData(backupData)).getValue() + + const invalidItems = otherContext.items.invalidItems + expect(invalidItems.length).to.equal(0) + + expect(result.affectedItems.length).to.equal(backupData.items.length) + + const itemsKey = result.affectedItems.find((item) => item.content_type === ContentType.TYPES.KeySystemItemsKey) + expect(itemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + + const importedNote = result.affectedItems.find((item) => item.content_type === ContentType.TYPES.Note) + expect(importedNote.key_system_identifier).to.equal(vault.systemIdentifier) + expect(importedNote.uuid).to.equal(note.uuid) + + const importedRootKey = result.affectedItems.find( + (item) => item.content_type === ContentType.TYPES.KeySystemRootKey, + ) + expect(importedRootKey.systemIdentifier).to.equal(vault.systemIdentifier) + + await otherContext.deinit() + }) + }) }) }) diff --git a/packages/snjs/mocha/vaults/invites.test.js b/packages/snjs/mocha/vaults/invites.test.js index 29db7f51f..9b6fc9c39 100644 --- a/packages/snjs/mocha/vaults/invites.test.js +++ b/packages/snjs/mocha/vaults/invites.test.js @@ -21,6 +21,7 @@ describe('shared vault invites', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should invite contact to vault', async () => { diff --git a/packages/snjs/mocha/vaults/items.test.js b/packages/snjs/mocha/vaults/items.test.js index 74e34e3e3..f106024a1 100644 --- a/packages/snjs/mocha/vaults/items.test.js +++ b/packages/snjs/mocha/vaults/items.test.js @@ -22,6 +22,7 @@ describe('shared vault items', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should add item to shared vault with no other members', async () => { diff --git a/packages/snjs/mocha/vaults/key-management.test.js b/packages/snjs/mocha/vaults/key-management.test.js index 3ad9ea166..1a2ab666e 100644 --- a/packages/snjs/mocha/vaults/key-management.test.js +++ b/packages/snjs/mocha/vaults/key-management.test.js @@ -21,6 +21,7 @@ describe('vault key management', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) describe('locking', () => { diff --git a/packages/snjs/mocha/vaults/key-rotation.test.js b/packages/snjs/mocha/vaults/key-rotation.test.js index d5bfb29fd..617be8228 100644 --- a/packages/snjs/mocha/vaults/key-rotation.test.js +++ b/packages/snjs/mocha/vaults/key-rotation.test.js @@ -22,6 +22,7 @@ describe('vault key rotation', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('should reencrypt all items keys belonging to key system', async () => { diff --git a/packages/snjs/mocha/vaults/key-sharing.test.js b/packages/snjs/mocha/vaults/key-sharing.test.js index a8be0bbb7..52a1a3597 100644 --- a/packages/snjs/mocha/vaults/key-sharing.test.js +++ b/packages/snjs/mocha/vaults/key-sharing.test.js @@ -22,6 +22,7 @@ describe('vault key sharing', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => { diff --git a/packages/snjs/mocha/vaults/keypair-change.test.js b/packages/snjs/mocha/vaults/keypair-change.test.js index 16814446c..943870296 100644 --- a/packages/snjs/mocha/vaults/keypair-change.test.js +++ b/packages/snjs/mocha/vaults/keypair-change.test.js @@ -22,6 +22,7 @@ describe('keypair change', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('contacts should be able to handle receiving multiple keypair changed messages and trust them in order', async () => { diff --git a/packages/snjs/mocha/vaults/limits.test.js b/packages/snjs/mocha/vaults/limits.test.js index be8b819a1..7fedd85d4 100644 --- a/packages/snjs/mocha/vaults/limits.test.js +++ b/packages/snjs/mocha/vaults/limits.test.js @@ -22,6 +22,7 @@ describe('shared vault limits', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) describe('free users', () => { diff --git a/packages/snjs/mocha/vaults/permissions.test.js b/packages/snjs/mocha/vaults/permissions.test.js index f229c01fa..844ba0308 100644 --- a/packages/snjs/mocha/vaults/permissions.test.js +++ b/packages/snjs/mocha/vaults/permissions.test.js @@ -22,6 +22,7 @@ describe('shared vault permissions', function () { await context.deinit() localStorage.clear() sinon.restore() + context = undefined }) it('non-admin user should not be able to invite user', async () => { diff --git a/packages/snjs/mocha/vaults/pkc.test.js b/packages/snjs/mocha/vaults/pkc.test.js index 703ac30ac..5b5d0184e 100644 --- a/packages/snjs/mocha/vaults/pkc.test.js +++ b/packages/snjs/mocha/vaults/pkc.test.js @@ -20,9 +20,7 @@ describe('public key cryptography', function () { afterEach(async () => { await context.deinit() localStorage.clear() - sinon.restore() - context = undefined }) diff --git a/packages/snjs/mocha/vaults/shared_vaults.test.js b/packages/snjs/mocha/vaults/shared_vaults.test.js index 54984b82a..3749869b2 100644 --- a/packages/snjs/mocha/vaults/shared_vaults.test.js +++ b/packages/snjs/mocha/vaults/shared_vaults.test.js @@ -8,7 +8,6 @@ describe('shared vaults', function () { this.timeout(Factory.TwentySecondTimeout) let context - let vaults beforeEach(async function () { localStorage.clear() @@ -17,8 +16,6 @@ describe('shared vaults', function () { await context.launch() await context.register() - - vaults = context.vaults }) afterEach(async function () { @@ -39,7 +36,7 @@ describe('shared vaults', function () { description: 'new vault description', }) - const updatedVault = vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + const updatedVault = context.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) expect(updatedVault.name).to.equal('new vault name') expect(updatedVault.description).to.equal('new vault description') diff --git a/packages/snjs/mocha/vaults/vaults.test.js b/packages/snjs/mocha/vaults/vaults.test.js index 04b4206af..7b02caed7 100644 --- a/packages/snjs/mocha/vaults/vaults.test.js +++ b/packages/snjs/mocha/vaults/vaults.test.js @@ -7,7 +7,6 @@ describe('vaults', function () { this.timeout(Factory.TwentySecondTimeout) let context - let vaults beforeEach(async function () { localStorage.clear() @@ -15,8 +14,6 @@ describe('vaults', function () { context = await Factory.createVaultsContextWithFakeCrypto() await context.launch() - - vaults = context.vaults }) afterEach(async function () { @@ -24,12 +21,11 @@ describe('vaults', function () { localStorage.clear() sinon.restore() context = undefined - vaults = undefined }) describe('offline', function () { it('should be able to create an offline vault', async () => { - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) @@ -45,7 +41,7 @@ describe('vaults', function () { it('should be able to create an offline vault with app passcode', async () => { await context.application.addPasscode('123') - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) @@ -60,12 +56,12 @@ describe('vaults', function () { }) it('should add item to offline vault', async () => { - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const item = await context.createSyncedNote() - await vaults.moveItemToVault(vault, item) + await context.vaults.moveItemToVault(vault, item) const updatedItem = context.items.findItem(item.uuid) expect(updatedItem.key_system_identifier).to.equal(vault.systemIdentifier) @@ -73,11 +69,11 @@ describe('vaults', function () { it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => { const appIdentifier = context.identifier - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const note = await context.createSyncedNote('foo', 'bar') - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) await context.deinit() const recreatedContext = await Factory.createVaultsContextWithFakeCrypto(appIdentifier) @@ -93,11 +89,11 @@ describe('vaults', function () { describe('porting from offline to online', () => { it('should maintain vault system identifiers across items after registration', async () => { const appIdentifier = context.identifier - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const note = await context.createSyncedNote('foo', 'bar') - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) await context.register() await context.sync() @@ -120,11 +116,11 @@ describe('vaults', function () { it('should decrypt vault items', async () => { const appIdentifier = context.identifier - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const note = await context.createSyncedNote('foo', 'bar') - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) await context.register() await context.sync() @@ -149,7 +145,7 @@ describe('vaults', function () { }) it('should create a vault', async () => { - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) expect(vault).to.not.be.undefined @@ -164,11 +160,11 @@ describe('vaults', function () { it('should add item to vault', async () => { const note = await context.createSyncedNote('foo', 'bar') - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) const updatedNote = context.items.findItem(note.uuid) expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier) @@ -177,11 +173,11 @@ describe('vaults', function () { describe('client timing', () => { it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => { const appIdentifier = context.identifier - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const note = await context.createSyncedNote('foo', 'bar') - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) await context.deinit() const recreatedContext = await Factory.createVaultsContextWithFakeCrypto(appIdentifier) @@ -197,13 +193,13 @@ describe('vaults', function () { describe('key system root key rotation', () => { it('rotating a key system root key should create a new vault items key', async () => { - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const keySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0] - await vaults.rotateVaultRootKey(vault) + await context.vaults.rotateVaultRootKey(vault) const updatedKeySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0] @@ -212,13 +208,13 @@ describe('vaults', function () { }) it('deleting a vault should delete all its items', async () => { - const vault = await vaults.createRandomizedVault({ + const vault = await context.vaults.createRandomizedVault({ name: 'My Vault', }) const note = await context.createSyncedNote('foo', 'bar') - await vaults.moveItemToVault(vault, note) + await context.vaults.moveItemToVault(vault, note) - await vaults.deleteVault(vault) + await context.vaults.deleteVault(vault) const updatedNote = context.items.findItem(note.uuid) expect(updatedNote).to.be.undefined diff --git a/packages/ui-services/src/Archive/ArchiveManager.ts b/packages/ui-services/src/Archive/ArchiveManager.ts index 5bd19334c..62848a46b 100644 --- a/packages/ui-services/src/Archive/ArchiveManager.ts +++ b/packages/ui-services/src/Archive/ArchiveManager.ts @@ -39,14 +39,16 @@ export class ArchiveManager { } public async downloadBackup(encrypted: boolean): Promise { - const data = encrypted - ? await this.application.createEncryptedBackupFile() - : await this.application.createDecryptedBackupFile() + const result = encrypted + ? await this.application.createEncryptedBackupFile.execute() + : await this.application.createDecryptedBackupFile.execute() - if (!data) { + if (result.isFailed()) { return } + const data = result.getValue() + const blobData = new Blob([JSON.stringify(data, null, 2)], { type: 'text/json', }) diff --git a/packages/web/src/javascripts/Application/Device/DesktopManager.ts b/packages/web/src/javascripts/Application/Device/DesktopManager.ts index 484937b28..8d02069eb 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopManager.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopManager.ts @@ -103,15 +103,15 @@ export class DesktopManager private async getBackupFile(): Promise { const encrypted = this.application.hasProtectionSources() - const data = encrypted - ? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() - : await this.application.createDecryptedBackupFile() + const result = encrypted + ? await this.application.createEncryptedBackupFile.execute({ skipAuthorization: true }) + : await this.application.createDecryptedBackupFile.execute() - if (data) { - return JSON.stringify(data, null, 2) + if (result.isFailed()) { + return undefined } - return undefined + return JSON.stringify(result.getValue(), null, 2) } getExtServerHost(): string { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx index e8cdaf9fd..661c6b251 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx @@ -58,14 +58,16 @@ const DataBackups = ({ application }: Props) => { }, [refreshEncryptionStatus]) const downloadDataArchive = async () => { - const data = isBackupEncrypted - ? await application.createEncryptedBackupFile() - : await application.createDecryptedBackupFile() + const result = isBackupEncrypted + ? await application.createEncryptedBackupFile.execute() + : await application.createDecryptedBackupFile.execute() - if (!data) { + if (result.isFailed()) { return } + const data = result.getValue() + const blobData = new Blob([JSON.stringify(data, null, 2)], { type: 'text/json', }) @@ -126,15 +128,11 @@ const DataBackups = ({ application }: Props) => { setIsImportDataLoading(false) - if (!result) { - return - } - let statusText = STRING_IMPORT_SUCCESS - if ('error' in result) { - statusText = result.error.text - } else if (result.errorCount) { - statusText = StringImportError(result.errorCount) + if (result.isFailed()) { + statusText = result.getError() + } else if (result.getValue().errorCount) { + statusText = StringImportError(result.getValue().errorCount) } void alertDialog({ text: statusText,