146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
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<ImportDataResult> {
|
|
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<Result<ImportDataResult>> {
|
|
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<void> {
|
|
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<Result<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]>> {
|
|
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<void> {
|
|
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()
|
|
}
|
|
}
|