diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts index 9c0b802f2d1..1a60b5ce1b6 100644 --- a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts @@ -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 diff --git a/packages/models/src/Domain/Runtime/Deltas/FileImport.ts b/packages/models/src/Domain/Runtime/Deltas/FileImport.ts index 042ea0cb7dc..0444ac01333 100644 --- a/packages/models/src/Domain/Runtime/Deltas/FileImport.ts +++ b/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' @@ -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, ) {} @@ -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. diff --git a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts index a07ef756ffa..befeae1ceb7 100644 --- a/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts +++ b/packages/services/src/Domain/Encryption/EncryptionProviderInterface.ts @@ -89,7 +89,6 @@ export interface EncryptionProviderInterface { setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise createNewItemsKeyWithRollback(): Promise<() => Promise> - reencryptApplicableItemsAfterUserRootKeyChange(): Promise getSureDefaultItemsKey(): ItemsKeyInterface createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface diff --git a/packages/services/src/Domain/Encryption/EncryptionService.ts b/packages/services/src/Domain/Encryption/EncryptionService.ts index c1119c6ced7..1d46594c359 100644 --- a/packages/services/src/Domain/Encryption/EncryptionService.ts +++ b/packages/services/src/Domain/Encryption/EncryptionService.ts @@ -240,10 +240,6 @@ export class EncryptionService return this.itemsEncryption.repersistAllItems() } - public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise { - await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange() - } - public async createNewItemsKeyWithRollback(): Promise<() => Promise> { return this._createNewItemsKeyWithRollback.execute() } diff --git a/packages/services/src/Domain/Encryption/UseCase/TypeA/ReencryptTypeAItems.ts b/packages/services/src/Domain/Encryption/UseCase/TypeA/ReencryptTypeAItems.ts new file mode 100644 index 00000000000..6085494550d --- /dev/null +++ b/packages/services/src/Domain/Encryption/UseCase/TypeA/ReencryptTypeAItems.ts @@ -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 { + constructor(private items: ItemManagerInterface, private mutator: MutatorClientInterface) {} + + public async execute(): Promise> { + 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() + } +} diff --git a/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts index 2854555086b..0eaa67f03f7 100644 --- a/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts +++ b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts @@ -76,9 +76,7 @@ export class KeySystemKeyManager } } - public getRootKeyFromStorageForVault( - keySystemIdentifier: KeySystemIdentifier, - ): KeySystemRootKeyInterface | undefined { + getRootKeyFromStorageForVault(keySystemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined { const payload = this.storage.getValue>( this.storageKeyForRootKey(keySystemIdentifier), ) @@ -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}` } diff --git a/packages/services/src/Domain/Mutator/ImportDataUseCase.ts b/packages/services/src/Domain/Mutator/ImportDataUseCase.ts index 199be706514..88fb4dcbe77 100644 --- a/packages/services/src/Domain/Mutator/ImportDataUseCase.ts +++ b/packages/services/src/Domain/Mutator/ImportDataUseCase.ts @@ -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 = { @@ -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 { 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())) { @@ -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 - 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 } @@ -143,7 +111,42 @@ export class ImportDataUseCase { return { affectedItems: affectedItems, - errorCount: decryptedPayloadsOrError.length - validPayloads.length, + 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/Payloads/PayloadManagerInterface.ts b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts index 24b8e224525..d21d6c626e0 100644 --- a/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts +++ b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts @@ -3,7 +3,6 @@ import { EncryptedPayloadInterface, FullyFormedPayloadInterface, PayloadEmitSource, - DecryptedPayloadInterface, HistoryMap, } from '@standardnotes/models' import { IntegrityPayload } from '@standardnotes/responses' @@ -24,7 +23,7 @@ export interface PayloadManagerInterface { */ get nonDeletedItems(): FullyFormedPayloadInterface[] - importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise + importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void } diff --git a/packages/services/src/Domain/RootKeyManager/RootKeyManager.ts b/packages/services/src/Domain/RootKeyManager/RootKeyManager.ts index 789cee74bfe..70a35f41b46 100644 --- a/packages/services/src/Domain/RootKeyManager/RootKeyManager.ts +++ b/packages/services/src/Domain/RootKeyManager/RootKeyManager.ts @@ -13,7 +13,6 @@ import { EncryptionOperatorsInterface, } from '@standardnotes/encryption' import { - ContentTypesUsingRootKeyEncryption, DecryptedPayload, DecryptedTransferPayload, EncryptedPayload, @@ -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 { private rootKey?: RootKeyInterface @@ -47,10 +45,9 @@ export class RootKeyManager extends AbstractService { 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) @@ -58,6 +55,12 @@ export class RootKeyManager extends AbstractService { 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 } @@ -307,7 +310,7 @@ export class RootKeyManager extends AbstractService { 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) } @@ -473,19 +476,4 @@ export class RootKeyManager extends AbstractService { 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 { - 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) - } - } } diff --git a/packages/services/src/Domain/User/UserService.spec.ts b/packages/services/src/Domain/User/UserService.spec.ts index 7905519057a..735e4e4130b 100644 --- a/packages/services/src/Domain/User/UserService.spec.ts +++ b/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' @@ -25,6 +26,7 @@ describe('UserService', () => { let challengeService: ChallengeServiceInterface let protectionService: ProtectionsClientInterface let userApiService: UserApiServiceInterface + let reencryptTypeAItems: ReencryptTypeAItems let internalEventBus: InternalEventBusInterface const createService = () => @@ -38,6 +40,7 @@ describe('UserService', () => { challengeService, protectionService, userApiService, + reencryptTypeAItems, internalEventBus, ) diff --git a/packages/services/src/Domain/User/UserService.ts b/packages/services/src/Domain/User/UserService.ts index ac4ecfeec40..f44f3aa90ef 100644 --- a/packages/services/src/Domain/User/UserService.ts +++ b/packages/services/src/Domain/User/UserService.ts @@ -37,6 +37,7 @@ import { AccountEvent } from './AccountEvent' import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload' import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' +import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems' export class UserService extends AbstractService @@ -49,33 +50,48 @@ export class UserService private readonly MINIMUM_PASSWORD_LENGTH = 8 constructor( - private sessionManager: SessionsClientInterface, + private sessions: SessionsClientInterface, private sync: SyncServiceInterface, - private storageService: StorageServiceInterface, - private itemManager: ItemManagerInterface, - private encryptionService: EncryptionProviderInterface, - private alertService: AlertService, - private challengeService: ChallengeServiceInterface, - private protectionService: ProtectionsClientInterface, - private userApiService: UserApiServiceInterface, + private storage: StorageServiceInterface, + private items: ItemManagerInterface, + private encryption: EncryptionProviderInterface, + private alerts: AlertService, + private challenges: ChallengeServiceInterface, + private protections: ProtectionsClientInterface, + private userApi: UserApiServiceInterface, + private _reencryptTypeAItems: ReencryptTypeAItems, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) } + public override deinit(): void { + super.deinit() + ;(this.sessions as unknown) = undefined + ;(this.sync as unknown) = undefined + ;(this.storage as unknown) = undefined + ;(this.items as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.alerts as unknown) = undefined + ;(this.challenges as unknown) = undefined + ;(this.protections as unknown) = undefined + ;(this.userApi as unknown) = undefined + ;(this._reencryptTypeAItems as unknown) = undefined + } + async handleEvent(event: InternalEventInterface): Promise { if (event.type === AccountEvent.SignedInOrRegistered) { const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload this.sync.resetSyncState() - await this.storageService.setPersistencePolicy( + await this.storage.setPersistencePolicy( payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default, ) if (payload.mergeLocal) { await this.sync.markAllItemsAsNeedingSyncAndPersist() } else { - void this.itemManager.removeAllItemsFromMemory() + void this.items.removeAllItemsFromMemory() await this.clearDatabase() } @@ -88,37 +104,24 @@ export class UserService }) .then(() => { if (!payload.awaitSync) { - void this.encryptionService.decryptErroredPayloads() + void this.encryption.decryptErroredPayloads() } }) if (payload.awaitSync) { await syncPromise - await this.encryptionService.decryptErroredPayloads() + await this.encryption.decryptErroredPayloads() } } } - public override deinit(): void { - super.deinit() - ;(this.sessionManager as unknown) = undefined - ;(this.sync as unknown) = undefined - ;(this.storageService as unknown) = undefined - ;(this.itemManager as unknown) = undefined - ;(this.encryptionService as unknown) = undefined - ;(this.alertService as unknown) = undefined - ;(this.challengeService as unknown) = undefined - ;(this.protectionService as unknown) = undefined - ;(this.userApiService as unknown) = undefined - } - getUserUuid(): string { - return this.sessionManager.userUuid + return this.sessions.userUuid } isSignedIn(): boolean { - return this.sessionManager.isSignedIn() + return this.sessions.isSignedIn() } /** @@ -131,7 +134,7 @@ export class UserService ephemeral = false, mergeLocal = true, ): Promise { - if (this.encryptionService.hasAccount()) { + if (this.encryption.hasAccount()) { throw Error('Tried to register when an account already exists.') } @@ -143,7 +146,7 @@ export class UserService try { this.lockSyncing() - const response = await this.sessionManager.register(email, password, ephemeral) + const response = await this.sessions.register(email, password, ephemeral) await this.notifyEventSync(AccountEvent.SignedInOrRegistered, { payload: { @@ -177,7 +180,7 @@ export class UserService mergeLocal = true, awaitSync = false, ): Promise> { - if (this.encryptionService.hasAccount()) { + if (this.encryption.hasAccount()) { throw Error('Tried to sign in when an account already exists.') } @@ -191,7 +194,7 @@ export class UserService /** Prevent a timed sync from occuring while signing in. */ this.lockSyncing() - const { response } = await this.sessionManager.signIn(email, password, strict, ephemeral) + const { response } = await this.sessions.signIn(email, password, strict, ephemeral) if (!isErrorResponse(response)) { const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this) @@ -218,7 +221,7 @@ export class UserService message?: string }> { if ( - !(await this.protectionService.authorizeAction(ChallengeReason.DeleteAccount, { + !(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, { fallBackToAccountPassword: true, requireAccountPassword: true, forcePrompt: false, @@ -230,8 +233,8 @@ export class UserService } } - const uuid = this.sessionManager.getSureUser().uuid - const response = await this.userApiService.deleteAccount(uuid) + const uuid = this.sessions.getSureUser().uuid + const response = await this.userApi.deleteAccount(uuid) if (isErrorResponse(response)) { return { error: true, @@ -241,7 +244,7 @@ export class UserService await this.signOut(true) - void this.alertService.alert(InfoStrings.AccountDeleted) + void this.alerts.alert(InfoStrings.AccountDeleted) return { error: false, @@ -249,9 +252,9 @@ export class UserService } async submitUserRequest(requestType: UserRequestType): Promise { - const userUuid = this.sessionManager.getSureUser().uuid + const userUuid = this.sessions.getSureUser().uuid try { - const result = await this.userApiService.submitUserRequest({ + const result = await this.userApi.submitUserRequest({ userUuid, requestType, }) @@ -274,11 +277,7 @@ export class UserService public async correctiveSignIn(rootKey: SNRootKey): Promise> { this.lockSyncing() - const response = await this.sessionManager.bypassChecksAndSignInWithRootKey( - rootKey.keyParams.identifier, - rootKey, - false, - ) + const response = await this.sessions.bypassChecksAndSignInWithRootKey(rootKey.keyParams.identifier, rootKey, false) if (!isErrorResponse(response)) { await this.notifyEvent(AccountEvent.SignedInOrRegistered, { @@ -313,16 +312,16 @@ export class UserService }): Promise { const result = await this.performCredentialsChange(parameters) if (result.error) { - void this.alertService.alert(result.error.message) + void this.alerts.alert(result.error.message) } return result } public async signOut(force = false, source = DeinitSource.SignOut): Promise { const performSignOut = async () => { - await this.sessionManager.signOut() - await this.encryptionService.deleteWorkspaceSpecificKeyStateFromDevice() - await this.storageService.clearAllData() + await this.sessions.signOut() + await this.encryption.deleteWorkspaceSpecificKeyStateFromDevice() + await this.storage.clearAllData() await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } }) } @@ -332,10 +331,10 @@ export class UserService return } - const dirtyItems = this.itemManager.getDirtyItems() + const dirtyItems = this.items.getDirtyItems() if (dirtyItems.length > 0) { const singular = dirtyItems.length === 1 - const didConfirm = await this.alertService.confirm( + const didConfirm = await this.alerts.confirm( `There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${ singular ? 'item' : 'items' } with unsynced changes. If you sign out, these changes will be lost forever. Are you sure you want to sign out?`, @@ -353,7 +352,7 @@ export class UserService canceled?: true error?: { message: string } }> { - if (!this.sessionManager.isUserMissingKeyPair()) { + if (!this.sessions.isUserMissingKeyPair()) { throw Error('Cannot update account with first time keypair if user already has a keypair') } @@ -367,8 +366,8 @@ export class UserService canceled?: true error?: { message: string } }> { - const hasPasscode = this.encryptionService.hasPasscode() - const hasAccount = this.encryptionService.hasAccount() + const hasPasscode = this.encryption.hasPasscode() + const hasAccount = this.encryption.hasAccount() const prompts = [] if (hasPasscode) { prompts.push( @@ -389,11 +388,11 @@ export class UserService ) } const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true) - const response = await this.challengeService.promptForChallengeResponse(challenge) + const response = await this.challenges.promptForChallengeResponse(challenge) if (!response) { return { canceled: true } } - const dismissBlockingDialog = await this.alertService.blockingDialog( + const dismissBlockingDialog = await this.alerts.blockingDialog( Messages.DO_NOT_CLOSE_APPLICATION, Messages.UPGRADING_ENCRYPTION, ) @@ -436,11 +435,11 @@ export class UserService if (passcode.length < this.MINIMUM_PASSCODE_LENGTH) { return false } - if (!(await this.protectionService.authorizeAddingPasscode())) { + if (!(await this.protections.authorizeAddingPasscode())) { return false } - const dismissBlockingDialog = await this.alertService.blockingDialog( + const dismissBlockingDialog = await this.alerts.blockingDialog( Messages.DO_NOT_CLOSE_APPLICATION, Messages.SETTING_PASSCODE, ) @@ -453,11 +452,11 @@ export class UserService } public async removePasscode(): Promise { - if (!(await this.protectionService.authorizeRemovingPasscode())) { + if (!(await this.protections.authorizeRemovingPasscode())) { return false } - const dismissBlockingDialog = await this.alertService.blockingDialog( + const dismissBlockingDialog = await this.alerts.blockingDialog( Messages.DO_NOT_CLOSE_APPLICATION, Messages.REMOVING_PASSCODE, ) @@ -479,11 +478,11 @@ export class UserService if (newPasscode.length < this.MINIMUM_PASSCODE_LENGTH) { return false } - if (!(await this.protectionService.authorizeChangingPasscode())) { + if (!(await this.protections.authorizeChangingPasscode())) { return false } - const dismissBlockingDialog = await this.alertService.blockingDialog( + const dismissBlockingDialog = await this.alerts.blockingDialog( Messages.DO_NOT_CLOSE_APPLICATION, origination === KeyParamsOrigination.ProtocolUpgrade ? Messages.ProtocolUpgradeStrings.UpgradingPasscode @@ -499,7 +498,7 @@ export class UserService } public async populateSessionFromDemoShareToken(token: Base64String): Promise { - await this.sessionManager.populateSessionFromDemoShareToken(token) + await this.sessions.populateSessionFromDemoShareToken(token) await this.notifyEvent(AccountEvent.SignedInOrRegistered, { payload: { ephemeral: false, @@ -512,14 +511,14 @@ export class UserService private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) { const identifier = UuidGenerator.GenerateUuid() - const key = await this.encryptionService.createRootKey(identifier, passcode, origination) - await this.encryptionService.setNewRootKeyWrapper(key) + const key = await this.encryption.createRootKey(identifier, passcode, origination) + await this.encryption.setNewRootKeyWrapper(key) await this.rewriteItemsKeys() await this.sync.sync() } private async removePasscodeWithoutWarning() { - await this.encryptionService.removePasscode() + await this.encryption.removePasscode() await this.rewriteItemsKeys() } @@ -532,9 +531,9 @@ export class UserService * https://github.com/standardnotes/desktop/issues/131 */ private async rewriteItemsKeys(): Promise { - const itemsKeys = this.itemManager.getDisplayableItemsKeys() + const itemsKeys = this.items.getDisplayableItemsKeys() const payloads = itemsKeys.map((key) => key.payloadRepresentation()) - await this.storageService.deletePayloads(payloads) + await this.storage.deletePayloads(payloads) await this.sync.persistPayloads(payloads) } @@ -547,7 +546,7 @@ export class UserService } private clearDatabase(): Promise { - return this.storageService.clearAllPayloads() + return this.storage.clearAllPayloads() } private async performCredentialsChange(parameters: { @@ -558,7 +557,7 @@ export class UserService newPassword?: string passcode?: string }): Promise { - const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable(parameters.passcode) + const { wrappingKey, canceled } = await this.challenges.getWrappingKeyIfApplicable(parameters.passcode) if (canceled) { return { error: Error(Messages.CredentialsChangeStrings.PasscodeRequired) } @@ -572,14 +571,14 @@ export class UserService } } - const accountPasswordValidation = await this.encryptionService.validateAccountPassword(parameters.currentPassword) + const accountPasswordValidation = await this.encryption.validateAccountPassword(parameters.currentPassword) if (!accountPasswordValidation.valid) { return { error: Error(Messages.INVALID_PASSWORD), } } - const user = this.sessionManager.getUser() as User + const user = this.sessions.getUser() as User const currentEmail = user.email const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({ currentPassword: parameters.currentPassword, @@ -591,7 +590,7 @@ export class UserService this.lockSyncing() - const { response } = await this.sessionManager.changeCredentials({ + const { response } = await this.sessions.changeCredentials({ currentServerPassword: currentRootKey.serverPassword as string, newRootKey: newRootKey, wrappingKey, @@ -604,20 +603,20 @@ export class UserService return { error: Error(response.data.error?.message) } } - const rollback = await this.encryptionService.createNewItemsKeyWithRollback() - await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange() + const rollback = await this.encryption.createNewItemsKeyWithRollback() + await this._reencryptTypeAItems.execute() await this.sync.sync({ awaitAll: true }) - const defaultItemsKey = this.encryptionService.getSureDefaultItemsKey() + const defaultItemsKey = this.encryption.getSureDefaultItemsKey() const itemsKeyWasSynced = !defaultItemsKey.neverSynced if (!itemsKeyWasSynced) { - await this.sessionManager.changeCredentials({ + await this.sessions.changeCredentials({ currentServerPassword: newRootKey.serverPassword as string, newRootKey: currentRootKey, wrappingKey, }) - await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange() + await this._reencryptTypeAItems.execute() await rollback() await this.sync.sync({ awaitAll: true }) @@ -634,11 +633,11 @@ export class UserService newEmail?: string newPassword?: string }): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> { - const currentRootKey = await this.encryptionService.computeRootKey( + const currentRootKey = await this.encryption.computeRootKey( parameters.currentPassword, - (await this.encryptionService.getRootKeyParams()) as SNRootKeyParams, + this.encryption.getRootKeyParams() as SNRootKeyParams, ) - const newRootKey = await this.encryptionService.createRootKey( + const newRootKey = await this.encryption.createRootKey( parameters.newEmail ?? parameters.currentEmail, parameters.newPassword ?? parameters.currentPassword, parameters.origination, diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 2367bdd3e90..8fd8a2056de 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -75,6 +75,7 @@ export * from './Encryption/UseCase/TypeA/DecryptPayload' export * from './Encryption/UseCase/TypeA/DecryptPayloadWithKeyLookup' export * from './Encryption/UseCase/TypeA/EncryptPayload' export * from './Encryption/UseCase/TypeA/EncryptPayloadWithKeyLookup' +export * from './Encryption/UseCase/TypeA/ReencryptTypeAItems' export * from './Event/ApplicationEvent' export * from './Event/ApplicationEventCallback' export * from './Event/ApplicationStageChangedEventPayload' diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index 7c6eab7937b..97dd9744831 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -119,6 +119,7 @@ import { DeleteContact, VaultLockService, RemoveItemsFromMemory, + ReencryptTypeAItems, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -202,6 +203,10 @@ export class Dependencies { } private registerUseCaseMakers() { + this.factory.set(TYPES.ReencryptTypeAItems, () => { + return new ReencryptTypeAItems(this.get(TYPES.ItemManager), this.get(TYPES.MutatorService)) + }) + this.factory.set(TYPES.ImportDataUseCase, () => { return new ImportDataUseCase( this.get(TYPES.ItemManager), @@ -616,10 +621,9 @@ export class Dependencies { return new RootKeyManager( this.get(TYPES.DeviceInterface), this.get(TYPES.DiskStorageService), - this.get(TYPES.ItemManager), - this.get(TYPES.MutatorService), this.get(TYPES.EncryptionOperators), this.options.identifier, + this.get(TYPES.ReencryptTypeAItems), this.get(TYPES.InternalEventBus), ) }) @@ -1086,6 +1090,7 @@ export class Dependencies { this.get(TYPES.ChallengeService), this.get(TYPES.ProtectionService), this.get(TYPES.UserApiService), + this.get(TYPES.ReencryptTypeAItems), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 6e6a8ba1d3d..49c192763dd 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -151,6 +151,7 @@ export const TYPES = { DecryptBackupFile: Symbol.for('DecryptBackupFile'), IsVaultOwner: Symbol.for('IsVaultOwner'), RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'), + ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/lib/Migrations/Versions/2_202_1.ts b/packages/snjs/lib/Migrations/Versions/2_202_1.ts index 152e930af44..19ce8b5f9ea 100644 --- a/packages/snjs/lib/Migrations/Versions/2_202_1.ts +++ b/packages/snjs/lib/Migrations/Versions/2_202_1.ts @@ -14,7 +14,6 @@ export class Migration2_202_1 extends Migration { this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => { await this.migrateComponentDataToUserPreferences() await this.migrateActiveComponentsToUserPreferences() - await this.deleteComponentsWhichAreNativeFeatures() this.markDone() }) @@ -70,29 +69,4 @@ export class Migration2_202_1 extends Migration { await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes)) await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents)) } - - private async deleteComponentsWhichAreNativeFeatures(): Promise { - const componentsToDelete = [ - ...this.services.itemManager.getItems(ContentType.TYPES.Component), - ...this.services.itemManager.getItems(ContentType.TYPES.Theme), - ].filter((candidate) => { - const nativeFeature = FindNativeFeature(candidate.identifier) - if (!nativeFeature) { - return false - } - - const isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained = nativeFeature.deprecated - if (isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained) { - return false - } - - return true - }) - - if (componentsToDelete.length === 0) { - return - } - - await this.services.mutator.setItemsToBeDeleted(componentsToDelete) - } } diff --git a/packages/snjs/lib/Services/Payloads/PayloadManager.ts b/packages/snjs/lib/Services/Payloads/PayloadManager.ts index 1f3b6e0829c..42cbbc1fe42 100644 --- a/packages/snjs/lib/Services/Payloads/PayloadManager.ts +++ b/packages/snjs/lib/Services/Payloads/PayloadManager.ts @@ -286,13 +286,11 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt /** * Imports an array of payloads from an external source (such as a backup file) * and marks the items as dirty. - * @returns Resulting items */ - public async importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise { + public async importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise { const sourcedPayloads = payloads.map((p) => p.copy(undefined, PayloadSource.FileImport)) const delta = new DeltaFileImport(this.getMasterCollection(), sourcedPayloads, historyMap) - const emit = delta.result() await this.emitDeltaEmit(emit) diff --git a/packages/snjs/mocha/TestRegistry/VaultTests.js b/packages/snjs/mocha/TestRegistry/VaultTests.js index bfd27aff293..c8bcb25175d 100644 --- a/packages/snjs/mocha/TestRegistry/VaultTests.js +++ b/packages/snjs/mocha/TestRegistry/VaultTests.js @@ -6,6 +6,7 @@ export const VaultTests = { 'vaults/pkc.test.js', 'vaults/contacts.test.js', 'vaults/crypto.test.js', + 'vaults/importing.test.js', 'vaults/asymmetric-messages.test.js', 'vaults/keypair-change.test.js', 'vaults/signatures.test.js', @@ -16,7 +17,7 @@ export const VaultTests = { 'vaults/conflicts.test.js', 'vaults/deletion.test.js', 'vaults/permissions.test.js', - 'vaults/key_rotation.test.js', + 'vaults/key-rotation.test.js', 'vaults/files.test.js', ], } diff --git a/packages/snjs/mocha/keys.test.js b/packages/snjs/mocha/keys.test.js index e1f08e5106c..40a6d73bfc0 100644 --- a/packages/snjs/mocha/keys.test.js +++ b/packages/snjs/mocha/keys.test.js @@ -755,7 +755,7 @@ describe('keys', function () { currentServerPassword: currentRootKey.serverPassword, newRootKey, }) - await this.application.encryption.reencryptApplicableItemsAfterUserRootKeyChange() + await this.application.dependencies.get(TYPES.ReencryptTypeAItems).execute() /** Note: this may result in a deadlock if features_service syncs and results in an error */ await this.application.sync.sync({ awaitAll: true }) diff --git a/packages/snjs/mocha/lib/AppContext.js b/packages/snjs/mocha/lib/AppContext.js index d7ba122db58..14b64755ccd 100644 --- a/packages/snjs/mocha/lib/AppContext.js +++ b/packages/snjs/mocha/lib/AppContext.js @@ -369,6 +369,17 @@ export class AppContext { }) } + spyOnFunctionResult(object, functionName) { + return new Promise((resolve) => { + sinon.stub(object, functionName).callsFake(async (params) => { + object[functionName].restore() + const result = await object[functionName](params) + resolve(result) + return result + }) + }) + } + resolveWhenAsymmetricMessageProcessingCompletes() { return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages') } diff --git a/packages/snjs/mocha/migrations/migration.test.js b/packages/snjs/mocha/migrations/migration.test.js index 7d4caff8c11..85f36304280 100644 --- a/packages/snjs/mocha/migrations/migration.test.js +++ b/packages/snjs/mocha/migrations/migration.test.js @@ -121,71 +121,4 @@ describe('migrations', () => { await Factory.safeDeinit(application) }) - - describe('2.202.1', () => { - let application - - beforeEach(async () => { - application = await Factory.createAppWithRandNamespace() - - await application.prepareForLaunch({ - receiveChallenge: () => {}, - }) - await application.launch(true) - }) - - afterEach(async () => { - await Factory.safeDeinit(application) - }) - - it('remove components that are available as native features', async function () { - const editor = CreateDecryptedItemFromPayload( - new DecryptedPayload({ - uuid: '123', - content_type: ContentType.TYPES.Component, - content: FillItemContent({ - package_info: { - identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor, - }, - }), - }), - ) - await application.mutator.insertItem(editor) - await application.sync.sync() - - expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1) - - /** Run migration */ - const migration = new Migration2_202_1(application.migrations.services) - await migration.handleStage(ApplicationStage.FullSyncCompleted_13) - await application.sync.sync() - - expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(0) - }) - - it('do not remove components that are available as native features but deprecated', async function () { - const editor = CreateDecryptedItemFromPayload( - new DecryptedPayload({ - uuid: '123', - content_type: ContentType.TYPES.Component, - content: FillItemContent({ - package_info: { - identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor, - }, - }), - }), - ) - await application.mutator.insertItem(editor) - await application.sync.sync() - - expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1) - - /** Run migration */ - const migration = new Migration2_202_1(application.migrations.services) - await migration.handleStage(ApplicationStage.FullSyncCompleted_13) - await application.sync.sync() - - expect(application.items.getItems(ContentType.TYPES.Component).length).to.equal(1) - }) - }) }) diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index 71d2c0c5c8f..5c4f33acfa1 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -882,8 +882,4 @@ describe('importing', function () { expect(application.items.referencesForItem(importedTag).length).to.equal(1) expect(application.items.itemsReferencingItem(importedNote).length).to.equal(1) }) - - it('should decrypt backup file which contains a vaulted note without a synced key system root key', async () => { - console.error('TODO: Implement this test') - }) }) diff --git a/packages/snjs/mocha/vaults/contacts.test.js b/packages/snjs/mocha/vaults/contacts.test.js index f17ce2d4d5d..42ba2205b66 100644 --- a/packages/snjs/mocha/vaults/contacts.test.js +++ b/packages/snjs/mocha/vaults/contacts.test.js @@ -101,7 +101,7 @@ describe('contacts', function () { await deinitContactContext() }) - it('should be able to refresh a contact using a collaborationID that includes full chain of previouos public keys', async () => { + it('should be able to refresh a contact using a collaborationID that includes full chain of previous public keys', async () => { console.error('TODO: implement test') }) }) diff --git a/packages/snjs/mocha/vaults/crypto.test.js b/packages/snjs/mocha/vaults/crypto.test.js index 8112a414b82..2f38b1be72c 100644 --- a/packages/snjs/mocha/vaults/crypto.test.js +++ b/packages/snjs/mocha/vaults/crypto.test.js @@ -35,12 +35,25 @@ describe('shared vault crypto', function () { expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined }) - it('changing user password should re-encrypt all key system root keys', async () => { - console.error('TODO: implement') - }) + it('changing user password should re-encrypt all key system root keys and contacts with new user root key', async () => { + await Collaboration.createPrivateVault(context) + const spy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer') + await context.changePassword('new_password') + + const payloads = await spy + const keyPayloads = payloads.filter( + (payload) => + payload.content_type === ContentType.TYPES.KeySystemRootKey || + payload.content_type === ContentType.TYPES.TrustedContact, + ) + expect(keyPayloads.length).to.equal(2) + + for (const payload of payloads) { + const keyParams = context.encryption.getEmbeddedPayloadAuthenticatedData(new EncryptedPayload(payload)).kp - it('changing user password should re-encrypt all trusted contacts', async () => { - console.error('TODO: implement') + const userKeyParams = context.encryption.getRootKeyParams().content + expect(keyParams).to.eql(userKeyParams) + } }) }) diff --git a/packages/snjs/mocha/vaults/importing.test.js b/packages/snjs/mocha/vaults/importing.test.js new file mode 100644 index 00000000000..5b391aa6333 --- /dev/null +++ b/packages/snjs/mocha/vaults/importing.test.js @@ -0,0 +1,58 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe.skip('vault importing', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + }) + + it('should import vaulted items with synced root key', async () => { + console.error('TODO: implement') + }) + + 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, + }) + + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, vault, note) + + const backupData = await context.application.createEncryptedBackupFileForAutomatedDesktopBackups() + + const otherContext = await Factory.createAppContextWithRealCrypto() + 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 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) + + await otherContext.deinit() + }) +}) diff --git a/packages/snjs/mocha/vaults/key_rotation.test.js b/packages/snjs/mocha/vaults/key-rotation.test.js similarity index 79% rename from packages/snjs/mocha/vaults/key_rotation.test.js rename to packages/snjs/mocha/vaults/key-rotation.test.js index 16774f114bd..213e676d590 100644 --- a/packages/snjs/mocha/vaults/key_rotation.test.js +++ b/packages/snjs/mocha/vaults/key-rotation.test.js @@ -4,7 +4,7 @@ import * as Collaboration from '../lib/Collaboration.js' chai.use(chaiAsPromised) const expect = chai.expect -describe('shared vault key rotation', function () { +describe('vault key rotation', function () { this.timeout(Factory.TwentySecondTimeout) let context @@ -29,17 +29,66 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() - const spy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption') + const callSpy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption') + const syncSpy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer') const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault) await promise + await syncSpy - expect(spy.callCount).to.equal(1) + expect(callSpy.callCount).to.equal(1) + + const payloads = await syncSpy + const keyPayloads = payloads.filter((payload) => payload.content_type === ContentType.TYPES.KeySystemItemsKey) + expect(keyPayloads.length).to.equal(2) + + const vaultRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) + + for (const payload of keyPayloads) { + const keyParams = context.encryption.getEmbeddedPayloadAuthenticatedData(new EncryptedPayload(payload)).kp + expect(keyParams).to.eql(vaultRootKey.keyParams) + } deinitContactContext() }) + it('should update value of local storage mode key', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + const beforeKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + + await context.vaults.rotateVaultRootKey(vault, 'test password') + + const afterKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + + expect(afterKey.keyParams.creationTimestamp).to.be.greaterThan(beforeKey.keyParams.creationTimestamp) + expect(afterKey.key).to.not.equal(beforeKey.key) + expect(afterKey.itemsKey).to.not.equal(beforeKey.itemsKey) + }) + + it('should update value of mem storage mode key', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const beforeKey = context.keys.getMemCachedRootKey(vault.systemIdentifier) + + await context.vaults.rotateVaultRootKey(vault, 'test password') + + const afterKey = context.keys.getMemCachedRootKey(vault.systemIdentifier) + + expect(afterKey.keyParams.creationTimestamp).to.be.greaterThan(beforeKey.keyParams.creationTimestamp) + expect(afterKey.key).to.not.equal(beforeKey.key) + expect(afterKey.itemsKey).to.not.equal(beforeKey.itemsKey) + }) + it("rotating a vault's key should send an asymmetric message to all members", async () => { const { sharedVault, contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) diff --git a/packages/snjs/mocha/vaults/shared_vaults.test.js b/packages/snjs/mocha/vaults/shared_vaults.test.js index b95f81b6792..8f06bb4f0e0 100644 --- a/packages/snjs/mocha/vaults/shared_vaults.test.js +++ b/packages/snjs/mocha/vaults/shared_vaults.test.js @@ -104,10 +104,27 @@ describe('shared vaults', function () { }) it('should convert a vault to a shared vault', async () => { - console.error('TODO') - }) + const privateVault = await context.vaults.createRandomizedVault({ + name: 'My Private Vault', + }) + + const note = await context.createSyncedNote('foo', 'bar') + await context.vaults.moveItemToVault(privateVault, note) + + const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault) + + const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( + context, + sharedVault, + ) + + await Collaboration.acceptAllInvites(thirdPartyContext) + + const contextNote = thirdPartyContext.items.findItem(note.uuid) + expect(contextNote).to.not.be.undefined + expect(contextNote.title).to.equal('foo') + expect(contextNote.text).to.equal(note.text) - it('should send metadata change message when changing name or description', async () => { - console.error('TODO') + await deinitThirdPartyContext() }) })