Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: vault tests 3 #2373

Merged
merged 4 commits into from Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -14,9 +14,9 @@ export class GetPayloadAuthenticatedDataDetachedUseCase {
execute(
encrypted: EncryptedOutputParameters,
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
const itemKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
const contentKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)

const authenticatedDataString = itemKeyComponents.authenticatedData
const authenticatedDataString = contentKeyComponents.authenticatedData

const result = this.parseStringUseCase.execute<
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
Expand Down
10 changes: 3 additions & 7 deletions packages/models/src/Domain/Runtime/Deltas/FileImport.ts
@@ -1,7 +1,6 @@
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { ConflictDelta } from './Conflict'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
import { FullyFormedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
import { HistoryMap } from '../History'
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
import { DeltaInterface } from './Abstract/DeltaInterface'
Expand All @@ -11,7 +10,7 @@ import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
export class DeltaFileImport implements DeltaInterface {
constructor(
readonly baseCollection: ImmutablePayloadCollection,
private readonly applyPayloads: DecryptedPayloadInterface[],
private readonly applyPayloads: FullyFormedPayloadInterface[],
protected readonly historyMap: HistoryMap,
) {}

Expand All @@ -31,10 +30,7 @@ export class DeltaFileImport implements DeltaInterface {
return result
}

private resolvePayload(
payload: DecryptedPayloadInterface | DeletedPayloadInterface,
currentResults: SyncDeltaEmit,
): SourcelessSyncDeltaEmit {
private resolvePayload(payload: FullyFormedPayloadInterface, currentResults: SyncDeltaEmit): SourcelessSyncDeltaEmit {
/**
* Check to see if we've already processed a payload for this id.
* If so, that would be the latest value, and not what's in the base collection.
Expand Down
Expand Up @@ -89,7 +89,6 @@ export interface EncryptionProviderInterface {
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>

createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
getSureDefaultItemsKey(): ItemsKeyInterface

createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
Expand Down
4 changes: 0 additions & 4 deletions packages/services/src/Domain/Encryption/EncryptionService.ts
Expand Up @@ -240,10 +240,6 @@ export class EncryptionService
return this.itemsEncryption.repersistAllItems()
}

public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
}

public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
return this._createNewItemsKeyWithRollback.execute()
}
Expand Down
@@ -0,0 +1,25 @@
import { MutatorClientInterface } from './../../../Mutator/MutatorClientInterface'
import { ItemManagerInterface } from './../../../Item/ItemManagerInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { ContentTypesUsingRootKeyEncryption } from '@standardnotes/models'

/**
* When the user root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
*/
export class ReencryptTypeAItems implements UseCaseInterface<void> {
constructor(private items: ItemManagerInterface, private mutator: MutatorClientInterface) {}

public async execute(): Promise<Result<void>> {
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
if (items.length > 0) {
/**
* Do not call sync after marking dirty.
* Re-encrypting items keys is called by consumers who have specific flows who
* will sync on their own timing
*/
await this.mutator.setItemsDirty(items)
}

return Result.ok()
}
}
8 changes: 5 additions & 3 deletions packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts
Expand Up @@ -76,9 +76,7 @@ export class KeySystemKeyManager
}
}

public getRootKeyFromStorageForVault(
keySystemIdentifier: KeySystemIdentifier,
): KeySystemRootKeyInterface | undefined {
getRootKeyFromStorageForVault(keySystemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined {
const payload = this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(
this.storageKeyForRootKey(keySystemIdentifier),
)
Expand All @@ -94,6 +92,10 @@ export class KeySystemKeyManager
return key
}

getMemCachedRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface {
return this.rootKeyMemoryCache[systemIdentifier]
}

private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
}
Expand Down
99 changes: 51 additions & 48 deletions packages/services/src/Domain/Mutator/ImportDataUseCase.ts
Expand Up @@ -9,18 +9,16 @@ import { ProtocolVersion, compareVersions } from '@standardnotes/common'
import {
BackupFile,
BackupFileDecryptedContextualPayload,
ComponentContent,
CopyPayloadWithContentOverride,
CreateDecryptedBackupFileContextPayload,
CreateEncryptedBackupFileContextPayload,
DecryptedItemInterface,
DecryptedPayloadInterface,
isDecryptedPayload,
isEncryptedPayload,
isEncryptedTransferPayload,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
import { ContentType } from '@standardnotes/domain-core'
import { Result } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'

const Strings = {
Expand Down Expand Up @@ -57,44 +55,22 @@ export class ImportDataUseCase {
* .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) }
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) {
/** 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') }
const passwordResult = await this.getFilePassword()
if (passwordResult.isFailed()) {
return { error: new ClientDisplayableError(passwordResult.getError()) }
}
this.challengeService.completeChallenge(challenge)
password = passwordResponse?.values[0].value as string
password = passwordResult.getValue()
}

if (!(await this.protectionService.authorizeFileImport())) {
Expand All @@ -110,31 +86,23 @@ export class ImportDataUseCase {
})

const decryptedPayloadsOrError = await this._decryptBackFile.execute(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.TYPES.Component && (payload.content as ComponentContent).active) {
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
return CopyPayloadWithContentOverride(typedContent, {
active: false,
})
} else {
return payload
}
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(
validPayloads,
importablePayloads,
this.historyService.getHistoryMapCopy(),
)

const promise = this.sync.sync()

if (awaitSync) {
await promise
}
Expand All @@ -143,7 +111,42 @@ export class ImportDataUseCase {

return {
affectedItems: affectedItems,
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
errorCount: decryptedPayloadsOrError.length - importablePayloads.length,
}
}

private async getFilePassword(): Promise<Result<string>> {
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<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()
}
}
Expand Up @@ -3,7 +3,6 @@ import {
EncryptedPayloadInterface,
FullyFormedPayloadInterface,
PayloadEmitSource,
DecryptedPayloadInterface,
HistoryMap,
} from '@standardnotes/models'
import { IntegrityPayload } from '@standardnotes/responses'
Expand All @@ -24,7 +23,7 @@ export interface PayloadManagerInterface {
*/
get nonDeletedItems(): FullyFormedPayloadInterface[]

importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>

removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
}
30 changes: 9 additions & 21 deletions packages/services/src/Domain/RootKeyManager/RootKeyManager.ts
Expand Up @@ -13,7 +13,6 @@ import {
EncryptionOperatorsInterface,
} from '@standardnotes/encryption'
import {
ContentTypesUsingRootKeyEncryption,
DecryptedPayload,
DecryptedTransferPayload,
EncryptedPayload,
Expand All @@ -32,12 +31,11 @@ import { StorageValueModes } from '../Storage/StorageTypes'
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
import { AbstractService } from '../Service/AbstractService'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
import { KeyMode } from './KeyMode'
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'

export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
private rootKey?: RootKeyInterface
Expand All @@ -47,17 +45,22 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
constructor(
private device: DeviceInterface,
private storage: StorageServiceInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private operators: EncryptionOperatorsInterface,
private identifier: ApplicationIdentifier,
private _reencryptTypeAItems: ReencryptTypeAItems,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
}

override deinit() {
super.deinit()
;(this.device as unknown) = undefined
;(this.storage as unknown) = undefined
;(this.operators as unknown) = undefined
;(this.identifier as unknown) = undefined
;(this._reencryptTypeAItems as unknown) = undefined

this.rootKey = undefined
this.memoizedRootKeyParams = undefined
}
Expand Down Expand Up @@ -307,7 +310,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
if (this.keyMode === KeyMode.WrapperOnly) {
this.setRootKeyInstance(wrappingKey)
await this.reencryptApplicableItemsAfterUserRootKeyChange()
await this._reencryptTypeAItems.execute()
} else {
await this.wrapAndPersistRootKey(wrappingKey)
}
Expand Down Expand Up @@ -473,19 +476,4 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
keyParams: keyParams.getPortableValue(),
})
}

/**
* When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
*/
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
if (items.length > 0) {
/**
* Do not call sync after marking dirty.
* Re-encrypting items keys is called by consumers who have specific flows who
* will sync on their own timing
*/
await this.mutator.setItemsDirty(items)
}
}
}
3 changes: 3 additions & 0 deletions packages/services/src/Domain/User/UserService.spec.ts
@@ -1,3 +1,4 @@
import { ReencryptTypeAItems } from './../Encryption/UseCase/TypeA/ReencryptTypeAItems'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { UserApiServiceInterface } from '@standardnotes/api'
import { UserRequestType } from '@standardnotes/common'
Expand Down Expand Up @@ -25,6 +26,7 @@ describe('UserService', () => {
let challengeService: ChallengeServiceInterface
let protectionService: ProtectionsClientInterface
let userApiService: UserApiServiceInterface
let reencryptTypeAItems: ReencryptTypeAItems
let internalEventBus: InternalEventBusInterface

const createService = () =>
Expand All @@ -38,6 +40,7 @@ describe('UserService', () => {
challengeService,
protectionService,
userApiService,
reencryptTypeAItems,
internalEventBus,
)

Expand Down