internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -0,0 +1,146 @@
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 { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
import {
BackupFile,
BackupFileDecryptedContextualPayload,
ComponentContent,
CopyPayloadWithContentOverride,
CreateDecryptedBackupFileContextPayload,
CreateEncryptedBackupFileContextPayload,
DecryptedItemInterface,
DecryptedPayloadInterface,
isDecryptedPayload,
isEncryptedTransferPayload,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
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 syncService: SyncServiceInterface,
private protectionService: ProtectionsClientInterface,
private encryption: EncryptionProviderInterface,
private payloadManager: PayloadManagerInterface,
private challengeService: ChallengeServiceInterface,
private historyService: HistoryServiceInterface,
) {}
/**
* @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<ImportDataReturnType> {
if (data.version) {
/**
* 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.
*/
const version = data.version as ProtocolVersion
const supportedVersions = this.encryption.supportedVersions()
if (!supportedVersions.includes(version)) {
return { error: new ClientDisplayableError(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 { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) }
}
}
let password: string | undefined
if (data.auth_params || data.keyParams) {
/** Get import file password. */
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 { error: new ClientDisplayableError('Import aborted') }
}
this.challengeService.completeChallenge(challenge)
password = passwordResponse?.values[0].value as string
}
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.encryption.decryptBackupFile(data, password)
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
return { error: decryptedPayloadsOrError }
}
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
/* Don't want to activate any components during import process in
* case of exceptions breaking up the import proccess */
if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) {
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
return CopyPayloadWithContentOverride(typedContent, {
active: false,
})
} else {
return payload
}
})
const affectedUuids = await this.payloadManager.importPayloads(
validPayloads,
this.historyService.getHistoryMapCopy(),
)
const promise = this.syncService.sync()
if (awaitSync) {
await promise
}
const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[]
return {
affectedItems: affectedItems,
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
}
}
}

View File

@@ -1,70 +1,92 @@
import { ContentType } from '@standardnotes/common'
import {
BackupFile,
ComponentMutator,
DecryptedItemInterface,
DecryptedItemMutator,
DecryptedPayload,
DecryptedPayloadInterface,
EncryptedItemInterface,
FeatureRepoMutator,
FileItem,
ItemContent,
ItemsKeyInterface,
ItemsKeyMutatorInterface,
MutationType,
PayloadEmitSource,
PredicateInterface,
SmartView,
SNComponent,
SNFeatureRepo,
SNNote,
SNTag,
TransactionalMutation,
VaultListingInterface,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
import { SyncOptions } from '../Sync/SyncOptions'
export interface MutatorClientInterface {
/**
* Inserts the input item by its payload properties, and marks the item as dirty.
* A sync is not performed after an item is inserted. This must be handled by the caller.
*/
insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty?: boolean): Promise<T>
emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
/**
* Mutates a pre-existing item, marks it as dirty, and syncs it
*/
changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemToLookupUuidFor: DecryptedItemInterface,
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<DecryptedItemInterface | undefined>
/**
* Mutates pre-existing items, marks them as dirty, and syncs
*/
changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise<void>
setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise<void>
setItemsDirty(
itemsToLookupUuidsFor: DecryptedItemInterface[],
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
isUserModified?: boolean,
): Promise<DecryptedItemInterface[]>
createItem<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
contentType: ContentType,
content: C,
needsSync?: boolean,
vault?: VaultListingInterface,
): Promise<T>
changeItem<
M extends DecryptedItemMutator = DecryptedItemMutator,
I extends DecryptedItemInterface = DecryptedItemInterface,
>(
itemToLookupUuidFor: I,
mutate?: (mutator: M) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
syncOptions?: SyncOptions,
): Promise<void>
payloadSourceKey?: string,
): Promise<I>
changeItems<
M extends DecryptedItemMutator = DecryptedItemMutator,
I extends DecryptedItemInterface = DecryptedItemInterface,
>(
itemsToLookupUuidsFor: I[],
mutate?: (mutator: M) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<I[]>
/**
* Mutates a pre-existing item and marks it as dirty. Does not sync changes.
*/
changeItem<M extends DecryptedItemMutator>(
itemToLookupUuidFor: DecryptedItemInterface,
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
): Promise<DecryptedItemInterface | undefined>
changeItemsKey(
itemToLookupUuidFor: ItemsKeyInterface,
mutate: (mutator: ItemsKeyMutatorInterface) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<ItemsKeyInterface>
/**
* Mutates a pre-existing items and marks them as dirty. Does not sync changes.
*/
changeItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
itemsToLookupUuidsFor: DecryptedItemInterface[],
mutate: (mutator: M) => void,
updateTimestamps?: boolean,
): Promise<(DecryptedItemInterface | undefined)[]>
changeComponent(
itemToLookupUuidFor: SNComponent,
mutate: (mutator: ComponentMutator) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<SNComponent>
changeFeatureRepo(
itemToLookupUuidFor: SNFeatureRepo,
mutate: (mutator: FeatureRepoMutator) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<SNFeatureRepo>
/**
* Run unique mutations per each item in the array, then only propagate all changes
@@ -83,44 +105,11 @@ export interface MutatorClientInterface {
payloadSourceKey?: string,
): Promise<DecryptedItemInterface | undefined>
protectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
items: I[],
): Promise<I[]>
unprotectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
items: I[],
reason: ChallengeReason,
): Promise<I[] | undefined>
protectNote(note: SNNote): Promise<SNNote>
unprotectNote(note: SNNote): Promise<SNNote | undefined>
protectNotes(notes: SNNote[]): Promise<SNNote[]>
unprotectNotes(notes: SNNote[]): Promise<SNNote[]>
protectFile(file: FileItem): Promise<FileItem>
unprotectFile(file: FileItem): Promise<FileItem | undefined>
/**
* Takes the values of the input item and emits it onto global state.
*/
mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
/**
* Creates an unmanaged item that can be added later.
*/
createTemplateItem<
C extends ItemContent = ItemContent,
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
>(
contentType: ContentType,
content?: C,
override?: Partial<DecryptedPayload<C>>,
): I
/**
* @param isUserModified Whether to change the modified date the user
* sees of the item.
@@ -135,7 +124,13 @@ export interface MutatorClientInterface {
emptyTrash(): Promise<void>
duplicateItem<T extends DecryptedItemInterface>(item: T, additionalContent?: Partial<T['content']>): Promise<T>
duplicateItem<T extends DecryptedItemInterface>(
itemToLookupUuidFor: T,
isConflict?: boolean,
additionalContent?: Partial<T['content']>,
): Promise<T>
addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
/**
* Migrates any tags containing a '.' character to sa chema-based heirarchy, removing
@@ -146,41 +141,35 @@ export interface MutatorClientInterface {
/**
* Establishes a hierarchical relationship between two tags.
*/
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<void>
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<SNTag>
/**
* Remove the tag parent.
*/
unsetTagParent(childTag: SNTag): Promise<void>
unsetTagParent(childTag: SNTag): Promise<SNTag>
findOrCreateTag(title: string): Promise<SNTag>
findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise<SNTag>
/** Creates and returns the tag but does not run sync. Callers must perform sync. */
createTagOrSmartView(title: string): Promise<SNTag | SmartView>
createTagOrSmartView<T extends SNTag | SmartView>(title: string, vault?: VaultListingInterface): Promise<T>
findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<SNTag>
/**
* Activates or deactivates a component, depending on its
* current state, and syncs.
*/
toggleComponent(component: SNComponent): Promise<void>
associateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem | undefined>
toggleTheme(theme: SNComponent): Promise<void>
disassociateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem>
renameFile(file: FileItem, name: string): Promise<FileItem>
/**
* @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.
*/
importData(
data: BackupFile,
awaitSync?: boolean,
): Promise<
| {
affectedItems: DecryptedItemInterface[]
errorCount: number
}
| {
error: ClientDisplayableError
}
>
unlinkItems(
itemA: DecryptedItemInterface<ItemContent>,
itemB: DecryptedItemInterface<ItemContent>,
): Promise<DecryptedItemInterface<ItemContent>>
createSmartView<T extends DecryptedItemInterface>(dto: {
title: string
predicate: PredicateInterface<T>
iconString?: string
vault?: VaultListingInterface
}): Promise<SmartView>
linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote>
linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem>
addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
}