From 7222ca7fd08b24d548c9a0f6b120a7f6940a97f3 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 26 Jul 2023 04:55:58 -0500 Subject: [PATCH] tests: vaults-2 (#2368) --- .../KeySystem/CreateRandomKeySystemRootKey.ts | 4 +- .../CreateUserInputKeySystemRootKey.ts | 4 +- ...sswordType.ts => KeySystemPasswordType.ts} | 2 +- .../KeySystemRootKeyParamsInterface.ts | 4 +- .../Runtime/Collection/Collection.spec.ts | 127 ++++++ .../Domain/Runtime/Collection/Collection.ts | 10 +- .../Syncable/VaultListing/VaultListing.ts | 4 +- .../VaultListing/VaultListingInterface.ts | 4 +- packages/models/src/Domain/index.ts | 2 +- .../ItemsKey/CreateNewItemsKeyWithRollback.ts | 14 +- .../src/Domain/Item/ItemManagerInterface.ts | 2 +- .../Domain/KeySystem/KeySystemKeyManager.ts | 59 ++- .../KeySystem/KeySystemKeyManagerInterface.ts | 12 +- .../SharedVaults/SharedVaultService.spec.ts | 3 + .../Domain/SharedVaults/SharedVaultService.ts | 4 +- .../UseCase/DeleteExternalSharedVault.ts | 6 +- .../Domain/Storage/StorageServiceInterface.ts | 7 +- .../UseCase/RemoveItemsFromMemory.spec.ts | 48 +++ .../Storage/UseCase/RemoveItemsFromMemory.ts | 26 ++ ...ItemsLocally.ts => DiscardItemsLocally.ts} | 4 +- .../Vault/UseCase/ChangeVaultKeyOptions.ts | 172 +++++--- .../Vault/UseCase/ChangeVaultKeyOptionsDTO.ts | 8 +- .../src/Domain/Vault/UseCase/CreateVault.ts | 10 +- .../Domain/Vault/UseCase/RotateVaultKey.ts | 10 +- .../services/src/Domain/Vault/VaultService.ts | 11 +- .../src/Domain/Vault/VaultServiceInterface.ts | 5 +- .../src/Domain/VaultLock/VaultLockService.ts | 16 +- .../VaultLock/VaultLockServiceInterface.ts | 2 +- packages/services/src/Domain/index.ts | 3 +- .../Application/Dependencies/Dependencies.ts | 21 +- .../lib/Application/Dependencies/Types.ts | 3 +- .../snjs/lib/Migrations/Versions/2_20_0.ts | 2 +- .../snjs/lib/Migrations/Versions/2_36_0.ts | 2 +- .../snjs/lib/Services/Items/ItemManager.ts | 6 +- .../Services/Storage/DiskStorageService.ts | 32 +- .../snjs/mocha/TestRegistry/VaultTests.js | 2 +- packages/snjs/mocha/auth.test.js | 2 +- packages/snjs/mocha/item_manager.test.js | 2 +- packages/snjs/mocha/keys.test.js | 2 +- .../snjs/mocha/lib/web_device_interface.js | 10 + .../snjs/mocha/sync_tests/integrity.test.js | 4 +- .../snjs/mocha/vaults/key-management.test.js | 398 ++++++++++++++++++ .../snjs/mocha/vaults/key_rotation.test.js | 2 +- packages/snjs/mocha/vaults/locking.test.js | 108 ----- .../src/javascripts/Application/Database.ts | 5 +- .../Vaults/VaultModal/EditVaultModal.tsx | 12 +- .../VaultModal/PasswordTypePreference.tsx | 14 +- 47 files changed, 900 insertions(+), 310 deletions(-) rename packages/models/src/Domain/Local/KeyParams/{KeySystemRootKeyPasswordType.ts => KeySystemPasswordType.ts} (60%) create mode 100644 packages/models/src/Domain/Runtime/Collection/Collection.spec.ts create mode 100644 packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.spec.ts create mode 100644 packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.ts rename packages/services/src/Domain/UseCase/{RemoveItemsLocally.ts => DiscardItemsLocally.ts} (86%) create mode 100644 packages/snjs/mocha/vaults/key-management.test.js delete mode 100644 packages/snjs/mocha/vaults/locking.test.js diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts index 542d3a72b..4c0518103 100644 --- a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts @@ -3,7 +3,7 @@ import { V004Algorithm } from '../../../../Algorithm' import { KeySystemRootKeyInterface, KeySystemRootKeyParamsInterface, - KeySystemRootKeyPasswordType, + KeySystemPasswordType, } from '@standardnotes/models' import { ProtocolVersion } from '@standardnotes/common' import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey' @@ -20,7 +20,7 @@ export class CreateRandomKeySystemRootKey { const keyParams: KeySystemRootKeyParamsInterface = { systemIdentifier: dto.systemIdentifier, - passwordType: KeySystemRootKeyPasswordType.Randomized, + passwordType: KeySystemPasswordType.Randomized, creationTimestamp: new Date().getTime(), seed, version, diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts index 2ba88a6a3..818890c8a 100644 --- a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts @@ -4,7 +4,7 @@ import { KeySystemIdentifier, KeySystemRootKeyInterface, KeySystemRootKeyParamsInterface, - KeySystemRootKeyPasswordType, + KeySystemPasswordType, } from '@standardnotes/models' import { ProtocolVersion } from '@standardnotes/common' import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey' @@ -19,7 +19,7 @@ export class CreateUserInputKeySystemRootKey { const keyParams: KeySystemRootKeyParamsInterface = { systemIdentifier: dto.systemIdentifier, - passwordType: KeySystemRootKeyPasswordType.UserInputted, + passwordType: KeySystemPasswordType.UserInputted, creationTimestamp: new Date().getTime(), seed, version, diff --git a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts b/packages/models/src/Domain/Local/KeyParams/KeySystemPasswordType.ts similarity index 60% rename from packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts rename to packages/models/src/Domain/Local/KeyParams/KeySystemPasswordType.ts index 8c5a117c7..059e543ce 100644 --- a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts +++ b/packages/models/src/Domain/Local/KeyParams/KeySystemPasswordType.ts @@ -1,4 +1,4 @@ -export enum KeySystemRootKeyPasswordType { +export enum KeySystemPasswordType { UserInputted = 'user_inputted', Randomized = 'randomized', } diff --git a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts index 709100c8d..e8211d61a 100644 --- a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts +++ b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts @@ -1,6 +1,6 @@ import { ProtocolVersion } from '@standardnotes/common' import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier' -import { KeySystemRootKeyPasswordType } from './KeySystemRootKeyPasswordType' +import { KeySystemPasswordType } from './KeySystemPasswordType' /** * Key params are public data that contain information about how a root key was created. @@ -11,6 +11,6 @@ export interface KeySystemRootKeyParamsInterface { systemIdentifier: KeySystemIdentifier seed: string version: ProtocolVersion - passwordType: KeySystemRootKeyPasswordType + passwordType: KeySystemPasswordType creationTimestamp: number } diff --git a/packages/models/src/Domain/Runtime/Collection/Collection.spec.ts b/packages/models/src/Domain/Runtime/Collection/Collection.spec.ts new file mode 100644 index 000000000..ebf00df21 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Collection/Collection.spec.ts @@ -0,0 +1,127 @@ +import { + Collection, + DecryptedCollectionElement, + DeletedCollectionElement, + EncryptedCollectionElement, +} from './Collection' +import { FullyFormedPayloadInterface } from '../../Abstract/Payload' + +class TestCollection

extends Collection< + P, + DecryptedCollectionElement, + EncryptedCollectionElement, + DeletedCollectionElement +> {} + +describe('Collection', () => { + let collection: TestCollection + + beforeEach(() => { + collection = new TestCollection() + }) + + it('should initialize correctly', () => { + expect(collection.map).toEqual({}) + expect(collection.typedMap).toEqual({}) + expect(collection.referenceMap).toBeDefined() + expect(collection.conflictMap).toBeDefined() + }) + + it('should set and get element correctly', () => { + const testElement = { + uuid: 'test-uuid', + content_type: 'test-type', + content: {}, + references: [], + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement) + const element = collection.find('test-uuid') + + expect(element).toBe(testElement) + }) + + it('should check existence of an element correctly', () => { + const testElement = { + uuid: 'test-uuid', + content_type: 'test-type', + content: {}, + references: [], + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement) + const hasElement = collection.has('test-uuid') + + expect(hasElement).toBe(true) + }) + + it('should return all elements', () => { + const testElement1 = { + uuid: 'test-uuid-1', + content_type: 'test-type', + content: {}, + references: [], + } as unknown as FullyFormedPayloadInterface + + const testElement2 = { + uuid: 'test-uuid-2', + content_type: 'test-type', + content: {}, + references: [], + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement1) + collection.set(testElement2) + + const allElements = collection.all() + + expect(allElements).toEqual([testElement1, testElement2]) + }) + + it('should add uuid to invalidsIndex if element is error decrypting', () => { + const testElement = { + uuid: 'test-uuid', + content_type: 'test-type', + content: 'encrypted content', + errorDecrypting: true, + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement) + + expect(collection.invalidsIndex.has(testElement.uuid)).toBe(true) + }) + + it('should add uuid to invalidsIndex if element is encrypted', () => { + const testElement = { + uuid: 'test-uuid', + content_type: 'test-type', + content: 'encrypted content', + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement) + + expect(collection.invalidsIndex.has(testElement.uuid)).toBe(true) + }) + + it('should remove uuid from invalidsIndex if element is not encrypted', () => { + const testElement1 = { + uuid: 'test-uuid-1', + content_type: 'test-type', + content: 'encrypted content', + errorDecrypting: true, + } as unknown as FullyFormedPayloadInterface + + const testElement2 = { + uuid: 'test-uuid-1', + content_type: 'test-type', + content: {}, + references: [], + } as unknown as FullyFormedPayloadInterface + + collection.set(testElement1) + expect(collection.invalidsIndex.has(testElement1.uuid)).toBe(true) + + collection.set(testElement2) + expect(collection.invalidsIndex.has(testElement2.uuid)).toBe(false) + }) +}) diff --git a/packages/models/src/Domain/Runtime/Collection/Collection.ts b/packages/models/src/Domain/Runtime/Collection/Collection.ts index f16317c1d..5fd32a48a 100644 --- a/packages/models/src/Domain/Runtime/Collection/Collection.ts +++ b/packages/models/src/Domain/Runtime/Collection/Collection.ts @@ -59,7 +59,7 @@ export abstract class Collection< } isErrorDecryptingElement = (e: Decrypted | Encrypted | Deleted): e is Encrypted => { - return this.isEncryptedElement(e) && e.errorDecrypting === true + return this.isEncryptedElement(e) } isDeletedElement = (e: Decrypted | Encrypted | Deleted): e is Deleted => { @@ -78,10 +78,10 @@ export abstract class Collection< conflictMapCopy?: UuidMap, ) { if (copy) { - this.map = mapCopy! - this.typedMap = typedMapCopy! - this.referenceMap = referenceMapCopy! - this.conflictMap = conflictMapCopy! + this.map = mapCopy as Record + this.typedMap = typedMapCopy as Record + this.referenceMap = referenceMapCopy as UuidMap + this.conflictMap = conflictMapCopy as UuidMap } else { this.referenceMap = new UuidMap() this.conflictMap = new UuidMap() diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts index 92f316d3f..cc218b4f6 100644 --- a/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts @@ -2,7 +2,7 @@ import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item' import { DecryptedPayloadInterface } from '../../Abstract/Payload' import { HistoryEntryInterface } from '../../Runtime/History' import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' -import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType' +import { KeySystemPasswordType } from '../../Local/KeyParams/KeySystemPasswordType' import { SharedVaultListingInterface, VaultListingInterface } from './VaultListingInterface' import { VaultListingContent } from './VaultListingContent' import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' @@ -44,7 +44,7 @@ export class VaultListing extends DecryptedItem implements return incomingKeyTimestamp > baseKeyTimestamp ? ConflictStrategy.KeepApply : ConflictStrategy.KeepBase } - get keyPasswordType(): KeySystemRootKeyPasswordType { + get keyPasswordType(): KeySystemPasswordType { return this.rootKeyParams.passwordType } diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts index 709a3c933..a92d03043 100644 --- a/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts @@ -1,6 +1,6 @@ import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier' import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' -import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType' +import { KeySystemPasswordType } from '../../Local/KeyParams/KeySystemPasswordType' import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' import { VaultListingSharingInfo } from './VaultListingSharingInfo' import { VaultListingContent } from './VaultListingContent' @@ -17,7 +17,7 @@ export interface VaultListingInterface extends DecryptedItemInterface Promise> { - const currentDefaultItemsKey = this.findDefaultItemsKey.execute(this.items.getDisplayableItemsKeys()).getValue() - const newDefaultItemsKey = await this.createDefaultItemsKey.execute() + const currentDefaultItemsKey = this._findDefaultItemsKey.execute(this.items.getDisplayableItemsKeys()).getValue() + const newDefaultItemsKey = await this._createDefaultItemsKey.execute() const rollback = async () => { - await this.removeItemsLocally.execute([newDefaultItemsKey]) + await this._discardItemsLocally.execute([newDefaultItemsKey]) if (currentDefaultItemsKey) { await this.mutator.changeItem(currentDefaultItemsKey, (mutator) => { diff --git a/packages/services/src/Domain/Item/ItemManagerInterface.ts b/packages/services/src/Domain/Item/ItemManagerInterface.ts index 9f7441a19..edb26c970 100644 --- a/packages/services/src/Domain/Item/ItemManagerInterface.ts +++ b/packages/services/src/Domain/Item/ItemManagerInterface.ts @@ -80,7 +80,7 @@ export interface ItemManagerInterface extends AbstractService { ): T[] subItemsMatchingPredicates(items: T[], predicates: PredicateInterface[]): T[] removeAllItemsFromMemory(): Promise - removeItemsLocally(items: AnyItemInterface[]): void + removeItemsFromMemory(items: AnyItemInterface[]): void getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[] getTagLongTitle(tag: SNTag): string getSortedTagsForItem(item: DecryptedItemInterface): SNTag[] diff --git a/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts index db65c747f..285455508 100644 --- a/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts +++ b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts @@ -1,3 +1,4 @@ +import { RemoveItemsFromMemory } from './../Storage/UseCase/RemoveItemsFromMemory' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { ApplicationStage } from './../Application/ApplicationStage' @@ -36,11 +37,20 @@ export class KeySystemKeyManager private readonly items: ItemManagerInterface, private readonly mutator: MutatorClientInterface, private readonly storage: StorageServiceInterface, + private readonly _removeItemsFromMemory: RemoveItemsFromMemory, eventBus: InternalEventBusInterface, ) { super(eventBus) } + public override deinit(): void { + ;(this.items as unknown) = undefined + ;(this.mutator as unknown) = undefined + ;(this.storage as unknown) = undefined + ;(this._removeItemsFromMemory as unknown) = undefined + super.deinit() + } + async handleEvent(event: InternalEventInterface): Promise { if (event.type === ApplicationEvent.ApplicationStageChanged) { const stage = (event.payload as ApplicationStageChangedEventPayload).stage @@ -60,9 +70,28 @@ export class KeySystemKeyManager const keyPayloads = keyRawPayloads.map((rawPayload) => new DecryptedPayload(rawPayload)) const keys = keyPayloads.map((payload) => new KeySystemRootKey(payload)) - keys.forEach((key) => { + + for (const key of keys) { this.rootKeyMemoryCache[key.systemIdentifier] = key - }) + } + } + + public getRootKeyFromStorageForVault( + keySystemIdentifier: KeySystemIdentifier, + ): KeySystemRootKeyInterface | undefined { + const payload = this.storage.getValue>( + this.storageKeyForRootKey(keySystemIdentifier), + ) + + if (!payload) { + return undefined + } + + const keyPayload = new DecryptedPayload(payload) + + const key = new KeySystemRootKey(keyPayload) + + return key } private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string { @@ -73,17 +102,14 @@ export class KeySystemKeyManager * When the key system root key changes, we must re-encrypt all vault items keys * with this new key system root key (by simply re-syncing). */ - public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise { + public async queueVaultItemsKeysForReencryption(keySystemIdentifier: KeySystemIdentifier): Promise { const keySystemItemsKeys = this.getKeySystemItemsKeys(keySystemIdentifier) if (keySystemItemsKeys.length > 0) { await this.mutator.setItemsDirty(keySystemItemsKeys) } } - public intakeNonPersistentKeySystemRootKey( - key: KeySystemRootKeyInterface, - storage: KeySystemRootKeyStorageMode, - ): void { + public cacheKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void { this.rootKeyMemoryCache[key.systemIdentifier] = key if (storage === KeySystemRootKeyStorageMode.Local) { @@ -91,7 +117,7 @@ export class KeySystemKeyManager } } - public undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void { + public removeKeyFromCache(systemIdentifier: KeySystemIdentifier): void { delete this.rootKeyMemoryCache[systemIdentifier] void this.storage.removeValue(this.storageKeyForRootKey(systemIdentifier)) } @@ -100,11 +126,11 @@ export class KeySystemKeyManager return this.items.getItems(ContentType.TYPES.KeySystemRootKey) } - public clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void { + public async wipeVaultKeysFromMemory(vault: VaultListingInterface): Promise { delete this.rootKeyMemoryCache[vault.systemIdentifier] const itemsKeys = this.getKeySystemItemsKeys(vault.systemIdentifier) - this.items.removeItemsLocally(itemsKeys) + await this._removeItemsFromMemory.execute(itemsKeys) } public getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] { @@ -131,19 +157,6 @@ export class KeySystemKeyManager await this.mutator.setItemsToBeDeleted(keys) } - public getKeySystemRootKeyWithToken( - systemIdentifier: KeySystemIdentifier, - rootKeyToken: string, - ): KeySystemRootKeyInterface | undefined { - const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier).filter((key) => key.token === rootKeyToken) - - if (keys.length > 1) { - throw new Error('Multiple synced key system root keys found for token') - } - - return keys[0] - } - public getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined { const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier) diff --git a/packages/services/src/Domain/KeySystem/KeySystemKeyManagerInterface.ts b/packages/services/src/Domain/KeySystem/KeySystemKeyManagerInterface.ts index ec367c81d..236ff3251 100644 --- a/packages/services/src/Domain/KeySystem/KeySystemKeyManagerInterface.ts +++ b/packages/services/src/Domain/KeySystem/KeySystemKeyManagerInterface.ts @@ -16,17 +16,13 @@ export interface KeySystemKeyManagerInterface { getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[] - getKeySystemRootKeyWithToken( - systemIdentifier: KeySystemIdentifier, - keyIdentifier: string, - ): KeySystemRootKeyInterface | undefined getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined - reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise + queueVaultItemsKeysForReencryption(keySystemIdentifier: KeySystemIdentifier): Promise - intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void - undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void + cacheKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void + removeKeyFromCache(systemIdentifier: KeySystemIdentifier): void - clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void + wipeVaultKeysFromMemory(vault: VaultListingInterface): Promise deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise } diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts index fa77f5109..ea847f8f3 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts @@ -1,3 +1,4 @@ +import { DiscardItemsLocally } from './../UseCase/DiscardItemsLocally' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults' import { IsVaultOwner } from './../VaultUser/UseCase/IsVaultOwner' @@ -42,6 +43,7 @@ describe('SharedVaultService', () => { const convertToSharedVault = {} as jest.Mocked const deleteSharedVaultUseCase = {} as jest.Mocked const isVaultAdmin = {} as jest.Mocked + const discardItemsLocally = {} as jest.Mocked const eventBus = {} as jest.Mocked eventBus.addEventHandler = jest.fn() @@ -62,6 +64,7 @@ describe('SharedVaultService', () => { convertToSharedVault, deleteSharedVaultUseCase, isVaultAdmin, + discardItemsLocally, eventBus, ) }) diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts index 346e8720a..51ee31428 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -1,3 +1,4 @@ +import { DiscardItemsLocally } from './../UseCase/DiscardItemsLocally' import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData' import { ClientDisplayableError, UserEventType } from '@standardnotes/responses' import { @@ -55,6 +56,7 @@ export class SharedVaultService private _convertToSharedVault: ConvertToSharedVault, private _deleteSharedVault: DeleteSharedVault, private _isVaultAdmin: IsVaultOwner, + private _discardItemsLocally: DiscardItemsLocally, eventBus: InternalEventBusInterface, ) { super(eventBus) @@ -132,7 +134,7 @@ export class SharedVaultService case UserEventType.SharedVaultItemRemoved: { const item = this.items.findItem(event.eventPayload.itemUuid) if (item) { - this.items.removeItemsLocally([item]) + void this._discardItemsLocally.execute([item]) } break } diff --git a/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts index beaeb2716..4917b23d6 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts @@ -2,7 +2,7 @@ import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' -import { RemoveItemsLocally } from '../../UseCase/RemoveItemsLocally' +import { DiscardItemsLocally } from '../../UseCase/DiscardItemsLocally' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' export class DeleteThirdPartyVault { @@ -11,7 +11,7 @@ export class DeleteThirdPartyVault { private mutator: MutatorClientInterface, private keys: KeySystemKeyManagerInterface, private sync: SyncServiceInterface, - private removeItemsLocally: RemoveItemsLocally, + private _discardItemsLocally: DiscardItemsLocally, ) {} async execute(vault: VaultListingInterface): Promise { @@ -33,7 +33,7 @@ export class DeleteThirdPartyVault { const itemsKeys = this.keys.getKeySystemItemsKeys(vault.systemIdentifier) - await this.removeItemsLocally.execute([...vaultItems, ...itemsKeys]) + await this._discardItemsLocally.execute([...vaultItems, ...itemsKeys]) } private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise { diff --git a/packages/services/src/Domain/Storage/StorageServiceInterface.ts b/packages/services/src/Domain/Storage/StorageServiceInterface.ts index 231f4d6d9..f0803f9a5 100644 --- a/packages/services/src/Domain/Storage/StorageServiceInterface.ts +++ b/packages/services/src/Domain/Storage/StorageServiceInterface.ts @@ -14,14 +14,17 @@ export interface StorageServiceInterface { getAllKeys(mode?: StorageValueModes): string[] getValue(key: string, mode?: StorageValueModes, defaultValue?: T): T canDecryptWithKey(key: RootKeyInterface): Promise - savePayload(payload: PayloadInterface): Promise - savePayloads(decryptedPayloads: PayloadInterface[]): Promise setValue(key: string, value: T, mode?: StorageValueModes): void removeValue(key: string, mode?: StorageValueModes): Promise setPersistencePolicy(persistencePolicy: StoragePersistencePolicies): Promise clearAllData(): Promise + + getRawPayloads(uuids: string[]): Promise + savePayload(payload: PayloadInterface): Promise + savePayloads(decryptedPayloads: PayloadInterface[]): Promise deletePayloads(payloads: FullyFormedPayloadInterface[]): Promise deletePayloadsWithUuids(uuids: string[]): Promise + clearAllPayloads(): Promise isEphemeralSession(): boolean } diff --git a/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.spec.ts b/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.spec.ts new file mode 100644 index 000000000..1f382d620 --- /dev/null +++ b/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.spec.ts @@ -0,0 +1,48 @@ +import { RemoveItemsFromMemory } from './RemoveItemsFromMemory' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { StorageServiceInterface } from '../StorageServiceInterface' +import { PayloadManagerInterface } from '../../Payloads/PayloadManagerInterface' +import { PayloadEmitSource, DecryptedItemInterface } from '@standardnotes/models' +import { Uuids } from '@standardnotes/utils' + +describe('RemoveItemsFromMemory', () => { + let storage: StorageServiceInterface + let items: ItemManagerInterface + let payloads: PayloadManagerInterface + let removeItemsFromMemory: RemoveItemsFromMemory + + beforeEach(() => { + storage = { + getRawPayloads: jest.fn().mockImplementation(() => Promise.resolve([])), + } as unknown as StorageServiceInterface + + items = { + removeItemsFromMemory: jest.fn(), + } as unknown as ItemManagerInterface + + payloads = { + emitPayloads: jest.fn().mockImplementation(() => Promise.resolve()), + } as unknown as PayloadManagerInterface + + removeItemsFromMemory = new RemoveItemsFromMemory(storage, items, payloads) + }) + + it('should execute removeItemsFromMemory use case correctly', async () => { + const testItems: DecryptedItemInterface[] = [ + { + uuid: 'uuid1', + content_type: 'type1', + }, + { + uuid: 'uuid2', + content_type: 'type2', + }, + ] + + await removeItemsFromMemory.execute(testItems) + + expect(items.removeItemsFromMemory).toHaveBeenCalledWith(testItems) + expect(storage.getRawPayloads).toHaveBeenCalledWith(Uuids(testItems)) + expect(payloads.emitPayloads).toHaveBeenCalledWith([], PayloadEmitSource.LocalDatabaseLoaded) + }) +}) diff --git a/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.ts b/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.ts new file mode 100644 index 000000000..fa12ec7f3 --- /dev/null +++ b/packages/services/src/Domain/Storage/UseCase/RemoveItemsFromMemory.ts @@ -0,0 +1,26 @@ +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { StorageServiceInterface } from '../StorageServiceInterface' +import { CreatePayload, DecryptedItemInterface, PayloadEmitSource, PayloadSource } from '@standardnotes/models' +import { Uuids } from '@standardnotes/utils' +import { PayloadManagerInterface } from '../../Payloads/PayloadManagerInterface' + +export class RemoveItemsFromMemory implements UseCaseInterface { + constructor( + private storage: StorageServiceInterface, + private items: ItemManagerInterface, + private payloads: PayloadManagerInterface, + ) {} + + async execute(items: DecryptedItemInterface[]): Promise> { + this.items.removeItemsFromMemory(items) + + const rawPayloads = await this.storage.getRawPayloads(Uuids(items)) + + const encryptedPayloads = rawPayloads.map((payload) => CreatePayload(payload, PayloadSource.LocalDatabaseLoaded)) + + await this.payloads.emitPayloads(encryptedPayloads, PayloadEmitSource.LocalDatabaseLoaded) + + return Result.ok() + } +} diff --git a/packages/services/src/Domain/UseCase/RemoveItemsLocally.ts b/packages/services/src/Domain/UseCase/DiscardItemsLocally.ts similarity index 86% rename from packages/services/src/Domain/UseCase/RemoveItemsLocally.ts rename to packages/services/src/Domain/UseCase/DiscardItemsLocally.ts index fea5032d2..591b51b88 100644 --- a/packages/services/src/Domain/UseCase/RemoveItemsLocally.ts +++ b/packages/services/src/Domain/UseCase/DiscardItemsLocally.ts @@ -3,11 +3,11 @@ import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { AnyItemInterface } from '@standardnotes/models' import { Uuids } from '@standardnotes/utils' -export class RemoveItemsLocally { +export class DiscardItemsLocally { constructor(private readonly items: ItemManagerInterface, private readonly storage: StorageServiceInterface) {} async execute(items: AnyItemInterface[]): Promise { - this.items.removeItemsLocally(items) + this.items.removeItemsFromMemory(items) await this.storage.deletePayloadsWithUuids(Uuids(items)) } diff --git a/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptions.ts b/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptions.ts index bef67da8e..60af590ca 100644 --- a/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptions.ts +++ b/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptions.ts @@ -1,6 +1,6 @@ import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services' import { - KeySystemRootKeyPasswordType, + KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface, VaultListingMutator, @@ -9,8 +9,9 @@ import { ChangeVaultKeyOptionsDTO } from './ChangeVaultKeyOptionsDTO' import { GetVault } from './GetVault' import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' +import { Result, UseCaseInterface } from '@standardnotes/domain-core' -export class ChangeVaultKeyOptions { +export class ChangeVaultKeyOptions implements UseCaseInterface { constructor( private mutator: MutatorClientInterface, private sync: SyncServiceInterface, @@ -19,59 +20,101 @@ export class ChangeVaultKeyOptions { private getVault: GetVault, ) {} - async execute(dto: ChangeVaultKeyOptionsDTO): Promise { - const useStorageMode = dto.newKeyStorageMode ?? dto.vault.keyStorageMode - + async execute(dto: ChangeVaultKeyOptionsDTO): Promise> { if (dto.newPasswordType) { - if (dto.vault.keyPasswordType === dto.newPasswordType.passwordType) { - throw new Error('Vault password type is already set to this type') - } - - if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.UserInputted) { - if (!dto.newPasswordType.userInputtedPassword) { - throw new Error('User inputted password is required') - } - await this.changePasswordTypeToUserInputted(dto.vault, dto.newPasswordType.userInputtedPassword, useStorageMode) - } else if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.Randomized) { - await this.changePasswordTypeToRandomized(dto.vault, useStorageMode) + const result = await this.handleNewPasswordType(dto) + if (result.isFailed()) { + return result } } - if (dto.newKeyStorageMode) { - const result = this.getVault.execute({ keySystemIdentifier: dto.vault.systemIdentifier }) - + if (dto.newStorageMode) { + const result = await this.handleNewStorageMode(dto) if (result.isFailed()) { - throw new Error('Vault not found') - } - - const latestVault = result.getValue() - - if (latestVault.rootKeyParams.passwordType !== KeySystemRootKeyPasswordType.UserInputted) { - throw new Error('Vault uses randomized password and cannot change its storage preference') - } - - if (dto.newKeyStorageMode === latestVault.keyStorageMode) { - throw new Error('Vault already uses this storage preference') - } - - if ( - dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Local || - dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Ephemeral - ) { - await this.changeStorageModeToLocalOrEphemeral(latestVault, dto.newKeyStorageMode) - } else if (dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Synced) { - await this.changeStorageModeToSynced(latestVault) + return result } } await this.sync.sync() + + return Result.ok() + } + + private async handleNewPasswordType(dto: ChangeVaultKeyOptionsDTO): Promise> { + if (!dto.newPasswordType) { + return Result.ok() + } + + if (dto.vault.keyPasswordType === dto.newPasswordType.passwordType) { + return Result.fail('Vault password type is already set to this type') + } + + if (dto.newPasswordType.passwordType === KeySystemPasswordType.UserInputted) { + if (!dto.newPasswordType.userInputtedPassword) { + return Result.fail('User inputted password is required') + } + const useStorageMode = dto.newStorageMode ?? dto.vault.keyStorageMode + const result = await this.changePasswordTypeToUserInputted( + dto.vault, + dto.newPasswordType.userInputtedPassword, + useStorageMode, + ) + if (result.isFailed()) { + return result + } + } else if (dto.newPasswordType.passwordType === KeySystemPasswordType.Randomized) { + const result = await this.changePasswordTypeToRandomized(dto.vault) + if (result.isFailed()) { + return result + } + } + + return Result.ok() + } + + private async handleNewStorageMode(dto: ChangeVaultKeyOptionsDTO): Promise> { + if (!dto.newStorageMode) { + return Result.ok() + } + + const result = this.getVault.execute({ keySystemIdentifier: dto.vault.systemIdentifier }) + if (result.isFailed()) { + return Result.fail('Vault not found') + } + + const latestVault = result.getValue() + + if (latestVault.rootKeyParams.passwordType !== KeySystemPasswordType.UserInputted) { + return Result.fail('Vault uses randomized password and cannot change its storage preference') + } + + if (dto.newStorageMode === latestVault.keyStorageMode) { + return Result.fail('Vault already uses this storage preference') + } + + if ( + dto.newStorageMode === KeySystemRootKeyStorageMode.Local || + dto.newStorageMode === KeySystemRootKeyStorageMode.Ephemeral + ) { + const result = await this.changeStorageModeToLocalOrEphemeral(latestVault, dto.newStorageMode) + if (result.isFailed()) { + return result + } + } else if (dto.newStorageMode === KeySystemRootKeyStorageMode.Synced) { + const result = await this.changeStorageModeToSynced(latestVault) + if (result.isFailed()) { + return result + } + } + + return Result.ok() } private async changePasswordTypeToUserInputted( vault: VaultListingInterface, userInputtedPassword: string, storageMode: KeySystemRootKeyStorageMode, - ): Promise { + ): Promise> { const newRootKey = this.encryption.createUserInputtedKeySystemRootKey({ systemIdentifier: vault.systemIdentifier, userInputtedPassword: userInputtedPassword, @@ -80,60 +123,73 @@ export class ChangeVaultKeyOptions { if (storageMode === KeySystemRootKeyStorageMode.Synced) { await this.mutator.insertItem(newRootKey, true) } else { - this.keys.intakeNonPersistentKeySystemRootKey(newRootKey, storageMode) + this.keys.cacheKey(newRootKey, storageMode) } await this.mutator.changeItem(vault, (mutator) => { mutator.rootKeyParams = newRootKey.keyParams }) - await this.keys.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier) + await this.keys.queueVaultItemsKeysForReencryption(vault.systemIdentifier) + + return Result.ok() } - private async changePasswordTypeToRandomized( - vault: VaultListingInterface, - storageMode: KeySystemRootKeyStorageMode, - ): Promise { + private async changePasswordTypeToRandomized(vault: VaultListingInterface): Promise> { + if (vault.keyStorageMode !== KeySystemRootKeyStorageMode.Synced) { + this.keys.removeKeyFromCache(vault.systemIdentifier) + + await this.mutator.changeItem(vault, (mutator) => { + mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced + }) + } + const newRootKey = this.encryption.createRandomizedKeySystemRootKey({ systemIdentifier: vault.systemIdentifier, }) - if (storageMode !== KeySystemRootKeyStorageMode.Synced) { - throw new Error('Cannot change to randomized password if root key storage is not synced') - } - await this.mutator.changeItem(vault, (mutator) => { mutator.rootKeyParams = newRootKey.keyParams }) await this.mutator.insertItem(newRootKey, true) - await this.keys.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier) + await this.keys.queueVaultItemsKeysForReencryption(vault.systemIdentifier) + + return Result.ok() } private async changeStorageModeToLocalOrEphemeral( vault: VaultListingInterface, - newKeyStorageMode: KeySystemRootKeyStorageMode, - ): Promise { + newStorageMode: KeySystemRootKeyStorageMode, + ): Promise> { const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier) if (!primaryKey) { - throw new Error('No primary key found') + return Result.fail('No primary key found') } - this.keys.intakeNonPersistentKeySystemRootKey(primaryKey, newKeyStorageMode) + if (newStorageMode === KeySystemRootKeyStorageMode.Ephemeral) { + this.keys.removeKeyFromCache(vault.systemIdentifier) + } + + this.keys.cacheKey(primaryKey, newStorageMode) await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier) await this.mutator.changeItem(vault, (mutator) => { - mutator.keyStorageMode = newKeyStorageMode + mutator.keyStorageMode = newStorageMode }) await this.sync.sync() + + return Result.ok() } - private async changeStorageModeToSynced(vault: VaultListingInterface): Promise { + private async changeStorageModeToSynced(vault: VaultListingInterface): Promise> { const allRootKeys = this.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) const syncedRootKeys = this.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + this.keys.removeKeyFromCache(vault.systemIdentifier) + for (const key of allRootKeys) { const existingSyncedKey = syncedRootKeys.find((syncedKey) => syncedKey.token === key.token) if (existingSyncedKey) { @@ -146,5 +202,7 @@ export class ChangeVaultKeyOptions { await this.mutator.changeItem(vault, (mutator) => { mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced }) + + return Result.ok() } } diff --git a/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptionsDTO.ts b/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptionsDTO.ts index af80d0624..c521b2865 100644 --- a/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptionsDTO.ts +++ b/packages/services/src/Domain/Vault/UseCase/ChangeVaultKeyOptionsDTO.ts @@ -1,10 +1,10 @@ -import { KeySystemRootKeyPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' +import { KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' export type ChangeVaultKeyOptionsDTO = { vault: VaultListingInterface newPasswordType: - | { passwordType: KeySystemRootKeyPasswordType.Randomized } - | { passwordType: KeySystemRootKeyPasswordType.UserInputted; userInputtedPassword: string } + | { passwordType: KeySystemPasswordType.Randomized } + | { passwordType: KeySystemPasswordType.UserInputted; userInputtedPassword: string } | undefined - newKeyStorageMode: KeySystemRootKeyStorageMode | undefined + newStorageMode: KeySystemRootKeyStorageMode | undefined } diff --git a/packages/services/src/Domain/Vault/UseCase/CreateVault.ts b/packages/services/src/Domain/Vault/UseCase/CreateVault.ts index b98dbcf56..a22431b5d 100644 --- a/packages/services/src/Domain/Vault/UseCase/CreateVault.ts +++ b/packages/services/src/Domain/Vault/UseCase/CreateVault.ts @@ -2,7 +2,7 @@ import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' import { UuidGenerator } from '@standardnotes/utils' import { KeySystemRootKeyParamsInterface, - KeySystemRootKeyPasswordType, + KeySystemPasswordType, VaultListingContentSpecialized, VaultListingInterface, KeySystemRootKeyStorageMode, @@ -44,9 +44,7 @@ export class CreateVault { keySystemIdentifier, vaultName: dto.vaultName, vaultDescription: dto.vaultDescription, - passwordType: dto.userInputtedPassword - ? KeySystemRootKeyPasswordType.UserInputted - : KeySystemRootKeyPasswordType.Randomized, + passwordType: dto.userInputtedPassword ? KeySystemPasswordType.UserInputted : KeySystemPasswordType.Randomized, rootKeyParams: rootKey.keyParams, storage: dto.storagePreference, }) @@ -60,7 +58,7 @@ export class CreateVault { keySystemIdentifier: string vaultName: string vaultDescription?: string - passwordType: KeySystemRootKeyPasswordType + passwordType: KeySystemPasswordType rootKeyParams: KeySystemRootKeyParamsInterface storage: KeySystemRootKeyStorageMode }): Promise { @@ -109,7 +107,7 @@ export class CreateVault { if (dto.storagePreference === KeySystemRootKeyStorageMode.Synced) { await this.mutator.insertItem(newRootKey, true) } else { - this.keys.intakeNonPersistentKeySystemRootKey(newRootKey, dto.storagePreference) + this.keys.cacheKey(newRootKey, dto.storagePreference) } return newRootKey diff --git a/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts b/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts index d3046116a..dc3e28dd3 100644 --- a/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts +++ b/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts @@ -3,7 +3,7 @@ import { ClientDisplayableError, isClientDisplayableError } from '@standardnotes import { KeySystemIdentifier, KeySystemRootKeyInterface, - KeySystemRootKeyPasswordType, + KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface, VaultListingMutator, @@ -31,7 +31,7 @@ export class RotateVaultKey { let newRootKey: KeySystemRootKeyInterface | undefined - if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.UserInputted) { if (!params.userInputtedPassword) { throw new Error('Cannot rotate key system root key; user inputted password required') } @@ -40,7 +40,7 @@ export class RotateVaultKey { systemIdentifier: params.vault.systemIdentifier, userInputtedPassword: params.userInputtedPassword, }) - } else if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.Randomized) { + } else if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.Randomized) { newRootKey = this.encryption.createRandomizedKeySystemRootKey({ systemIdentifier: params.vault.systemIdentifier, }) @@ -53,7 +53,7 @@ export class RotateVaultKey { if (params.vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) { await this.mutator.insertItem(newRootKey, true) } else { - this.keys.intakeNonPersistentKeySystemRootKey(newRootKey, params.vault.keyStorageMode) + this.keys.cacheKey(newRootKey, params.vault.keyStorageMode) } await this.mutator.changeItem(params.vault, (mutator) => { @@ -73,7 +73,7 @@ export class RotateVaultKey { errors.push(updateKeySystemItemsKeyResult) } - await this.keys.reencryptKeySystemItemsKeysForVault(params.vault.systemIdentifier) + await this.keys.queueVaultItemsKeysForReencryption(params.vault.systemIdentifier) return errors } diff --git a/packages/services/src/Domain/Vault/VaultService.ts b/packages/services/src/Domain/Vault/VaultService.ts index 50a75764b..0d797c721 100644 --- a/packages/services/src/Domain/Vault/VaultService.ts +++ b/packages/services/src/Domain/Vault/VaultService.ts @@ -26,6 +26,7 @@ import { MutatorClientInterface } from '../Mutator/MutatorClientInterface' import { AlertService } from '../Alert/AlertService' import { GetVaults } from './UseCase/GetVaults' import { VaultLockServiceInterface } from '../VaultLock/VaultLockServiceInterface' +import { Result } from '@standardnotes/domain-core' export class VaultService extends AbstractService @@ -194,7 +195,7 @@ export class VaultService return updatedVault } - async rotateVaultRootKey(vault: VaultListingInterface): Promise { + async rotateVaultRootKey(vault: VaultListingInterface, vaultPassword?: string): Promise { if (this.vaultLocks.isVaultLocked(vault)) { throw new Error('Cannot rotate root key of locked vault') } @@ -202,7 +203,7 @@ export class VaultService await this._rotateVaultKey.execute({ vault, sharedVaultUuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined, - userInputtedPassword: undefined, + userInputtedPassword: vaultPassword, }) await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault }) @@ -227,15 +228,17 @@ export class VaultService return this.getVault({ keySystemIdentifier: latestItem.key_system_identifier }) } - async changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise { + async changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise> { if (this.vaultLocks.isVaultLocked(dto.vault)) { throw new Error('Attempting to change vault options on a locked vault') } - await this._changeVaultKeyOptions.execute(dto) + const result = await this._changeVaultKeyOptions.execute(dto) if (dto.newPasswordType) { await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault: dto.vault }) } + + return result } } diff --git a/packages/services/src/Domain/Vault/VaultServiceInterface.ts b/packages/services/src/Domain/Vault/VaultServiceInterface.ts index 21c04f456..bd9c9f5e8 100644 --- a/packages/services/src/Domain/Vault/VaultServiceInterface.ts +++ b/packages/services/src/Domain/Vault/VaultServiceInterface.ts @@ -7,6 +7,7 @@ import { import { AbstractService } from '../Service/AbstractService' import { VaultServiceEvent, VaultServiceEventPayload } from './VaultServiceEvent' import { ChangeVaultKeyOptionsDTO } from './UseCase/ChangeVaultKeyOptionsDTO' +import { Result } from '@standardnotes/domain-core' export interface VaultServiceInterface extends AbstractService { @@ -34,6 +35,6 @@ export interface VaultServiceInterface vault: VaultListingInterface, params: { name: string; description: string }, ): Promise - rotateVaultRootKey(vault: VaultListingInterface): Promise - changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise + rotateVaultRootKey(vault: VaultListingInterface, vaultPassword?: string): Promise + changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise> } diff --git a/packages/services/src/Domain/VaultLock/VaultLockService.ts b/packages/services/src/Domain/VaultLock/VaultLockService.ts index 06603802b..38b902dd5 100644 --- a/packages/services/src/Domain/VaultLock/VaultLockService.ts +++ b/packages/services/src/Domain/VaultLock/VaultLockService.ts @@ -1,5 +1,5 @@ import { GetVaults } from '../Vault/UseCase/GetVaults' -import { KeySystemRootKeyPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' +import { KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' import { VaultLockServiceInterface } from './VaultLockServiceInterface' import { VaultLockServiceEvent, VaultLockServiceEventPayload } from './VaultLockServiceEvent' import { AbstractService } from '../Service/AbstractService' @@ -51,19 +51,19 @@ export class VaultLockService } public isVaultLockable(vault: VaultListingInterface): boolean { - return vault.keyPasswordType === KeySystemRootKeyPasswordType.UserInputted + return vault.keyPasswordType === KeySystemPasswordType.UserInputted } - public lockNonPersistentVault(vault: VaultListingInterface): void { + public async lockNonPersistentVault(vault: VaultListingInterface): Promise { if (vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) { throw new Error('Vault uses synced key storage and cannot be locked') } - if (vault.keyPasswordType !== KeySystemRootKeyPasswordType.UserInputted) { + if (vault.keyPasswordType !== KeySystemPasswordType.UserInputted) { throw new Error('Vault uses randomized password and cannot be locked') } - this.keys.clearMemoryOfKeysRelatedToVault(vault) + await this.keys.wipeVaultKeysFromMemory(vault) this.lockMap.set(vault.uuid, true) @@ -71,7 +71,7 @@ export class VaultLockService } public async unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise { - if (vault.keyPasswordType !== KeySystemRootKeyPasswordType.UserInputted) { + if (vault.keyPasswordType !== KeySystemPasswordType.UserInputted) { throw new Error('Vault uses randomized password and cannot be unlocked with user inputted password') } @@ -84,12 +84,12 @@ export class VaultLockService userInputtedPassword: password, }) - this.keys.intakeNonPersistentKeySystemRootKey(derivedRootKey, vault.keyStorageMode) + this.keys.cacheKey(derivedRootKey, vault.keyStorageMode) await this.encryption.decryptErroredPayloads() if (this.computeVaultLockState(vault) === 'locked') { - this.keys.undoIntakeNonPersistentKeySystemRootKey(vault.systemIdentifier) + this.keys.removeKeyFromCache(vault.systemIdentifier) return false } diff --git a/packages/services/src/Domain/VaultLock/VaultLockServiceInterface.ts b/packages/services/src/Domain/VaultLock/VaultLockServiceInterface.ts index 814961e18..d6188d086 100644 --- a/packages/services/src/Domain/VaultLock/VaultLockServiceInterface.ts +++ b/packages/services/src/Domain/VaultLock/VaultLockServiceInterface.ts @@ -7,6 +7,6 @@ export interface VaultLockServiceInterface getLockedvaults(): VaultListingInterface[] isVaultLocked(vault: VaultListingInterface): boolean isVaultLockable(vault: VaultListingInterface): boolean - lockNonPersistentVault(vault: VaultListingInterface): void + lockNonPersistentVault(vault: VaultListingInterface): Promise unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise } diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 888daa965..6d8a9ab0f 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -153,6 +153,7 @@ export * from './Storage/KeyValueStoreInterface' export * from './Storage/StorageKeys' export * from './Storage/StorageServiceInterface' export * from './Storage/StorageTypes' +export * from './Storage/UseCase/RemoveItemsFromMemory' export * from './Strings/InfoStrings' export * from './Strings/Messages' export * from './Subscription/AppleIAPProductId' @@ -166,7 +167,7 @@ export * from './Sync/SyncOptions' export * from './Sync/SyncQueueStrategy' export * from './Sync/SyncServiceInterface' export * from './Sync/SyncSource' -export * from './UseCase/RemoveItemsLocally' +export * from './UseCase/DiscardItemsLocally' export * from './User/AccountEvent' export * from './User/AccountEventData' export * from './User/CredentialsChangeFunctionResponse' diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index b0e0a994f..ce78e864b 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -47,7 +47,7 @@ import { IntegrityService, InternalEventBus, KeySystemKeyManager, - RemoveItemsLocally, + DiscardItemsLocally, RevisionManager, SelfContactManager, StatusService, @@ -118,6 +118,7 @@ import { ContactBelongsToVault, DeleteContact, VaultLockService, + RemoveItemsFromMemory, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -222,8 +223,16 @@ export class Dependencies { return new DecryptBackupFile(this.get(TYPES.EncryptionService)) }) - this.factory.set(TYPES.RemoveItemsLocally, () => { - return new RemoveItemsLocally(this.get(TYPES.ItemManager), this.get(TYPES.DiskStorageService)) + this.factory.set(TYPES.DiscardItemsLocally, () => { + return new DiscardItemsLocally(this.get(TYPES.ItemManager), this.get(TYPES.DiskStorageService)) + }) + + this.factory.set(TYPES.RemoveItemsFromMemory, () => { + return new RemoveItemsFromMemory( + this.get(TYPES.DiskStorageService), + this.get(TYPES.ItemManager), + this.get(TYPES.PayloadManager), + ) }) this.factory.set(TYPES.FindContact, () => { @@ -442,7 +451,7 @@ export class Dependencies { this.get(TYPES.MutatorService), this.get(TYPES.KeySystemKeyManager), this.get(TYPES.SyncService), - this.get(TYPES.RemoveItemsLocally), + this.get(TYPES.DiscardItemsLocally), ) }) @@ -555,7 +564,7 @@ export class Dependencies { this.get(TYPES.MutatorService), this.get(TYPES.ItemManager), this.get(TYPES.CreateNewDefaultItemsKey), - this.get(TYPES.RemoveItemsLocally), + this.get(TYPES.DiscardItemsLocally), this.get(TYPES.FindDefaultItemsKey), ) }) @@ -723,6 +732,7 @@ export class Dependencies { this.get(TYPES.ConvertToSharedVault), this.get(TYPES.DeleteSharedVault), this.get(TYPES.IsVaultOwner), + this.get(TYPES.DiscardItemsLocally), this.get(TYPES.InternalEventBus), ) }) @@ -1221,6 +1231,7 @@ export class Dependencies { this.get(TYPES.ItemManager), this.get(TYPES.MutatorService), this.get(TYPES.DiskStorageService), + this.get(TYPES.RemoveItemsFromMemory), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 3c8593e1b..6e6a8ba1d 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -89,7 +89,7 @@ export const TYPES = { GetRevision: Symbol.for('GetRevision'), DeleteRevision: Symbol.for('DeleteRevision'), ImportDataUseCase: Symbol.for('ImportDataUseCase'), - RemoveItemsLocally: Symbol.for('RemoveItemsLocally'), + DiscardItemsLocally: Symbol.for('DiscardItemsLocally'), FindContact: Symbol.for('FindContact'), GetAllContacts: Symbol.for('GetAllContacts'), CreateOrEditContact: Symbol.for('CreateOrEditContact'), @@ -150,6 +150,7 @@ export const TYPES = { EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'), DecryptBackupFile: Symbol.for('DecryptBackupFile'), IsVaultOwner: Symbol.for('IsVaultOwner'), + RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/lib/Migrations/Versions/2_20_0.ts b/packages/snjs/lib/Migrations/Versions/2_20_0.ts index b00ab3c36..edcf00133 100644 --- a/packages/snjs/lib/Migrations/Versions/2_20_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_20_0.ts @@ -18,7 +18,7 @@ export class Migration2_20_0 extends Migration { const items = this.services.itemManager.getItems(contentType) for (const item of items) { - this.services.itemManager.removeItemLocally(item) + this.services.itemManager.removeItemFromMemory(item) await this.services.storageService.deletePayloadWithUuid(item.uuid) } } diff --git a/packages/snjs/lib/Migrations/Versions/2_36_0.ts b/packages/snjs/lib/Migrations/Versions/2_36_0.ts index dbeb3e521..307167d18 100644 --- a/packages/snjs/lib/Migrations/Versions/2_36_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_36_0.ts @@ -18,7 +18,7 @@ export class Migration2_36_0 extends Migration { const items = this.services.itemManager.getItems(contentType) for (const item of items) { - this.services.itemManager.removeItemLocally(item) + this.services.itemManager.removeItemFromMemory(item) await this.services.storageService.deletePayloadWithUuid(item.uuid) } } diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index 7770d5f87..ed309f3a0 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -816,14 +816,14 @@ export class ItemManager extends Services.AbstractService implements Services.It /** * Important: Caller must coordinate with storage service separately to delete item from persistent database. */ - public removeItemLocally(item: Models.AnyItemInterface): void { - this.removeItemsLocally([item]) + public removeItemFromMemory(item: Models.AnyItemInterface): void { + this.removeItemsFromMemory([item]) } /** * Important: Caller must coordinate with storage service separately to delete item from persistent database. */ - public removeItemsLocally(items: Models.AnyItemInterface[]): void { + public removeItemsFromMemory(items: Models.AnyItemInterface[]): void { this.collection.discard(items) this.payloadManager.removePayloadLocally(items.map((item) => item.payload)) diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index a5d74b2c3..a5e0442fb 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -69,7 +69,7 @@ export class DiskStorageService private values!: StorageValuesObject constructor( - private deviceInterface: DeviceInterface, + private device: DeviceInterface, private identifier: string, protected override internalEventBus: InternalEventBusInterface, ) { @@ -82,7 +82,7 @@ export class DiskStorageService } public override deinit() { - ;(this.deviceInterface as unknown) = undefined + ;(this.device as unknown) = undefined ;(this.encryptionProvider as unknown) = undefined this.storagePersistable = false super.deinit() @@ -104,9 +104,9 @@ export class DiskStorageService this.persistencePolicy = persistencePolicy if (this.persistencePolicy === StoragePersistencePolicies.Ephemeral) { - await this.deviceInterface.clearNamespacedKeychainValue(this.identifier) - await this.deviceInterface.removeAllDatabaseEntries(this.identifier) - await this.deviceInterface.removeRawStorageValuesForIdentifier(this.identifier) + await this.device.clearNamespacedKeychainValue(this.identifier) + await this.device.removeAllDatabaseEntries(this.identifier) + await this.device.removeRawStorageValuesForIdentifier(this.identifier) await this.clearAllPayloads() } } @@ -116,7 +116,7 @@ export class DiskStorageService } public async initializeFromDisk(): Promise { - const value = await this.deviceInterface.getRawStorageValue(this.getPersistenceKey()) + const value = await this.device.getRawStorageValue(this.getPersistenceKey()) const values = value ? JSON.parse(value as string) : undefined await this.setInitialValues(values) @@ -240,7 +240,7 @@ export class DiskStorageService return values } - await this.deviceInterface?.setRawStorageValue(this.getPersistenceKey(), JSON.stringify(values)) + await this.device?.setRawStorageValue(this.getPersistenceKey(), JSON.stringify(values)) return values }) @@ -385,7 +385,11 @@ export class DiskStorageService } public async getAllRawPayloads(): Promise { - return this.deviceInterface.getAllDatabaseEntries(this.identifier) + return this.device.getAllDatabaseEntries(this.identifier) + } + + public async getRawPayloads(uuids: string[]): Promise { + return this.device.getDatabaseEntries(this.identifier, uuids) } public async savePayload(payload: FullyFormedPayloadInterface): Promise { @@ -440,7 +444,7 @@ export class DiskStorageService const exportedDeleted = deleted.map(CreateDeletedLocalStorageContextPayload) return this.executeCriticalFunction(async () => { - return this.deviceInterface?.saveDatabaseEntries( + return this.device?.saveDatabaseEntries( [...exportedEncrypted, ...exportedDecrypted, ...exportedDeleted], this.identifier, ) @@ -453,19 +457,19 @@ export class DiskStorageService public async deletePayloadsWithUuids(uuids: string[]): Promise { await this.executeCriticalFunction(async () => { - await Promise.all(uuids.map((uuid) => this.deviceInterface.removeDatabaseEntry(uuid, this.identifier))) + await Promise.all(uuids.map((uuid) => this.device.removeDatabaseEntry(uuid, this.identifier))) }) } public async deletePayloadWithUuid(uuid: string) { return this.executeCriticalFunction(async () => { - await this.deviceInterface.removeDatabaseEntry(uuid, this.identifier) + await this.device.removeDatabaseEntry(uuid, this.identifier) }) } public async clearAllPayloads() { return this.executeCriticalFunction(async () => { - return this.deviceInterface.removeAllDatabaseEntries(this.identifier) + return this.device.removeAllDatabaseEntries(this.identifier) }) } @@ -474,9 +478,9 @@ export class DiskStorageService await this.clearValues() await this.clearAllPayloads() - await this.deviceInterface.removeRawStorageValue(namespacedKey(this.identifier, RawStorageKey.SnjsVersion)) + await this.device.removeRawStorageValue(namespacedKey(this.identifier, RawStorageKey.SnjsVersion)) - await this.deviceInterface.removeRawStorageValue(this.getPersistenceKey()) + await this.device.removeRawStorageValue(this.getPersistenceKey()) }) } } diff --git a/packages/snjs/mocha/TestRegistry/VaultTests.js b/packages/snjs/mocha/TestRegistry/VaultTests.js index acb1b54a9..bfd27aff2 100644 --- a/packages/snjs/mocha/TestRegistry/VaultTests.js +++ b/packages/snjs/mocha/TestRegistry/VaultTests.js @@ -11,7 +11,7 @@ export const VaultTests = { 'vaults/signatures.test.js', 'vaults/shared_vaults.test.js', 'vaults/invites.test.js', - 'vaults/locking.test.js', + 'vaults/key-management.test.js', 'vaults/items.test.js', 'vaults/conflicts.test.js', 'vaults/deletion.test.js', diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index 61847252e..05c193fd0 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -449,7 +449,7 @@ describe('basic auth', function () { } const mutatorSpy = sinon.spy(this.application.mutator, 'setItemToBeDeleted') - const removeItemsSpy = sinon.spy(this.application.items, 'removeItemsLocally') + const removeItemsSpy = sinon.spy(this.application.items, 'removeItemsFromMemory') const deletePayloadsSpy = sinon.spy(this.application.storage, 'deletePayloadsWithUuids') await this.context.changePassword('new-password') diff --git a/packages/snjs/mocha/item_manager.test.js b/packages/snjs/mocha/item_manager.test.js index 20660a54a..78aa4811b 100644 --- a/packages/snjs/mocha/item_manager.test.js +++ b/packages/snjs/mocha/item_manager.test.js @@ -201,7 +201,7 @@ describe('item manager', function () { observed.push({ changed, inserted, removed, ignored }) }) const note = await createNote() - await application.items.removeItemLocally(note) + await application.items.removeItemFromMemory(note) expect(observed.length).to.equal(1) expect(application.items.findItem(note.uuid)).to.not.be.ok diff --git a/packages/snjs/mocha/keys.test.js b/packages/snjs/mocha/keys.test.js index 5a24141c6..e1f08e510 100644 --- a/packages/snjs/mocha/keys.test.js +++ b/packages/snjs/mocha/keys.test.js @@ -190,7 +190,7 @@ describe('keys', function () { const itemsKey = this.application.encryption.itemsKeyForEncryptedPayload(encryptedPayload) - await this.application.items.removeItemLocally(itemsKey) + await this.application.items.removeItemFromMemory(itemsKey) const erroredPayload = await this.application.encryption.decryptSplitSingle({ usesItemsKeyWithKeyLookup: { diff --git a/packages/snjs/mocha/lib/web_device_interface.js b/packages/snjs/mocha/lib/web_device_interface.js index 077e7fb19..b09cf6860 100644 --- a/packages/snjs/mocha/lib/web_device_interface.js +++ b/packages/snjs/mocha/lib/web_device_interface.js @@ -63,6 +63,16 @@ export default class WebDeviceInterface { return models } + async getDatabaseEntries(identifier, ids) { + const models = [] + for (const id of ids) { + const key = this._keyForPayloadId(id, identifier) + const model = JSON.parse(localStorage[key]) + models.push(model) + } + return models + } + async getDatabaseLoadChunks(options, identifier) { const entries = await this.getAllDatabaseEntries(identifier) const { diff --git a/packages/snjs/mocha/sync_tests/integrity.test.js b/packages/snjs/mocha/sync_tests/integrity.test.js index b290db335..704ec3095 100644 --- a/packages/snjs/mocha/sync_tests/integrity.test.js +++ b/packages/snjs/mocha/sync_tests/integrity.test.js @@ -53,7 +53,7 @@ describe('sync integrity', () => { const didEnterOutOfSync = awaitSyncEventPromise(this.application, SyncEvent.EnterOutOfSync) await this.application.sync.sync({ checkIntegrity: true }) - await this.application.items.removeItemLocally(item) + await this.application.items.removeItemFromMemory(item) await this.application.sync.sync({ checkIntegrity: true, awaitAll: true }) await didEnterOutOfSync @@ -70,7 +70,7 @@ describe('sync integrity', () => { const didExitOutOfSync = awaitSyncEventPromise(this.application, SyncEvent.ExitOutOfSync) await this.application.sync.sync({ checkIntegrity: true }) - await this.application.items.removeItemLocally(item) + await this.application.items.removeItemFromMemory(item) await this.application.sync.sync({ checkIntegrity: true, awaitAll: true }) await Promise.all([didEnterOutOfSync, didExitOutOfSync]) diff --git a/packages/snjs/mocha/vaults/key-management.test.js b/packages/snjs/mocha/vaults/key-management.test.js new file mode 100644 index 000000000..3e0fa6dd3 --- /dev/null +++ b/packages/snjs/mocha/vaults/key-management.test.js @@ -0,0 +1,398 @@ +import * as Factory from '../lib/factory.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('vault key management', 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() + }) + + it('should lock non-persistent vault', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + await context.vaultLocks.lockNonPersistentVault(vault) + + expect(context.vaultLocks.isVaultLocked(vault)).to.be.true + }) + + it('should not be able to lock user-inputted vault with synced key', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + await Factory.expectThrowsAsync( + () => context.vaultLocks.lockNonPersistentVault(vault), + 'Vault uses synced key storage and cannot be locked', + ) + }) + + it('should not be able to lock randomized vault', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + description: 'test vault description', + }) + + await Factory.expectThrowsAsync( + () => context.vaultLocks.lockNonPersistentVault(vault), + 'Vault uses synced key storage and cannot be locked', + ) + }) + + it('should throw if attempting to change password of locked vault', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + await context.vaultLocks.lockNonPersistentVault(vault) + + await Factory.expectThrowsAsync( + () => context.vaults.changeVaultOptions({ vault }), + 'Attempting to change vault options on a locked vault', + ) + }) + + describe('key rotation and persistence', () => { + it('rotating ephemeral vault should not persist keys', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + await context.vaults.rotateVaultRootKey(vault, 'test password') + + const syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(0) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + }) + + it('rotating local vault should not sync keys', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + await context.vaults.rotateVaultRootKey(vault, 'test password') + + const syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(0) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.not.be.undefined + }) + + it('rotating synced vault should sync new key', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + await context.vaults.rotateVaultRootKey(vault, 'test password') + + const syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(2) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + }) + }) + + describe('memory management', () => { + it('locking a vault should clear decrypted items keys from memory', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const itemsKeys = context.keys.getKeySystemItemsKeys(vault.systemIdentifier) + expect(itemsKeys.length).to.equal(1) + + await context.vaultLocks.lockNonPersistentVault(vault) + + const itemsKeysAfterLock = context.keys.getKeySystemItemsKeys(vault.systemIdentifier) + expect(itemsKeysAfterLock.length).to.equal(0) + }) + + it('locking then unlocking a vault should bring items keys back into memory', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + await context.vaultLocks.lockNonPersistentVault(vault) + await context.vaultLocks.unlockNonPersistentVault(vault, 'test password') + + const itemsKeys = context.keys.getKeySystemItemsKeys(vault.systemIdentifier) + expect(itemsKeys.length).to.equal(1) + + const rootKeys = context.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) + expect(rootKeys.length).to.equal(1) + }) + }) + + describe('changeVaultOptions', () => { + describe('change storage type', () => { + it('should not be able to change randomized vault from synced to local', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + description: 'test vault description', + }) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Local, + }) + + expect(result.isFailed()).to.be.true + expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') + }) + + it('should not be able to change randomized vault from synced to ephemeral', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + description: 'test vault description', + }) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Local, + }) + + expect(result.isFailed()).to.be.true + expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') + }) + + it('should change user password vault from synced to local', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Local, + }) + + expect(result.isFailed()).to.be.false + + syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(0) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.not.be.undefined + }) + + it('should change user password vault from synced to ephemeral', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Ephemeral, + }) + + expect(result.isFailed()).to.be.false + + syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(0) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + + const memKeys = context.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) + expect(memKeys.length).to.equal(1) + }) + + it('should change user password vault from local to synced', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Synced, + }) + + expect(result.isFailed()).to.be.false + + syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(1) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + + const memKeys = context.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) + expect(memKeys.length).to.equal(1) + }) + + it('should change user password vault from local to ephemeral', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + + const result = await context.vaults.changeVaultOptions({ + vault, + newStorageMode: KeySystemRootKeyStorageMode.Ephemeral, + }) + + expect(result.isFailed()).to.be.false + + syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(syncedKeys.length).to.equal(0) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + + const memKeys = context.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) + expect(memKeys.length).to.equal(1) + }) + }) + + describe('change password type', () => { + it('should fail to change password type from randomized to user inputted if password is not supplied', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + description: 'test vault description', + }) + + const result = await context.vaults.changeVaultOptions({ + vault, + newPasswordType: { + passwordType: KeySystemPasswordType.UserInputted, + }, + }) + + expect(result.isFailed()).to.be.true + }) + + it('should change password type from randomized to user inputted', async () => { + const vault = await context.vaults.createRandomizedVault({ + name: 'test vault', + description: 'test vault description', + }) + + const rootKeysBeforeChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(rootKeysBeforeChange.length).to.equal(1) + + const result = await context.vaults.changeVaultOptions({ + vault, + newPasswordType: { + passwordType: KeySystemPasswordType.UserInputted, + userInputtedPassword: 'test password', + }, + }) + + expect(result.isFailed()).to.be.false + + const rootKeysAfterChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(rootKeysAfterChange.length).to.equal(2) + + expect(rootKeysAfterChange[0].itemsKey).to.not.equal(rootKeysAfterChange[1].itemsKey) + }) + + it('should change password type from user inputted to randomized', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + const result = await context.vaults.changeVaultOptions({ + vault, + newPasswordType: { + passwordType: KeySystemPasswordType.Randomized, + }, + }) + + expect(result.isFailed()).to.be.false + + const rootKeysAfterChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + expect(rootKeysAfterChange.length).to.equal(1) + + const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) + expect(storedKey).to.be.undefined + + const updatedVault = context.vaults.getVault({ keySystemIdentifier: vault.systemIdentifier }) + expect(updatedVault.keyStorageMode).to.equal(KeySystemRootKeyStorageMode.Synced) + }) + + it('should fail to change password type from user inputted to randomized if storage mode is not synced', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Local, + }) + + const result = await context.vaults.changeVaultOptions({ + vault, + newPasswordType: { + passwordType: KeySystemPasswordType.Randomized, + }, + newStorageMode: KeySystemRootKeyStorageMode.Local, + }) + + expect(result.isFailed()).to.be.true + + expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') + }) + }) + }) +}) diff --git a/packages/snjs/mocha/vaults/key_rotation.test.js b/packages/snjs/mocha/vaults/key_rotation.test.js index c5094742c..16774f114 100644 --- a/packages/snjs/mocha/vaults/key_rotation.test.js +++ b/packages/snjs/mocha/vaults/key_rotation.test.js @@ -29,7 +29,7 @@ describe('shared vault key rotation', function () { contactContext.lockSyncing() - const spy = sinon.spy(context.keys, 'reencryptKeySystemItemsKeysForVault') + const spy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption') const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault) diff --git a/packages/snjs/mocha/vaults/locking.test.js b/packages/snjs/mocha/vaults/locking.test.js deleted file mode 100644 index f737e1f2a..000000000 --- a/packages/snjs/mocha/vaults/locking.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import * as Factory from '../lib/factory.js' -import * as Collaboration from '../lib/Collaboration.js' - -chai.use(chaiAsPromised) -const expect = chai.expect - -describe('vault locking', 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 lock non-persistent vault', async () => { - const vault = await context.vaults.createUserInputtedPasswordVault({ - name: 'test vault', - description: 'test vault description', - userInputtedPassword: 'test password', - storagePreference: KeySystemRootKeyStorageMode.Ephemeral, - }) - - context.vaultLocks.lockNonPersistentVault(vault) - - expect(context.vaultLocks.isVaultLocked(vault)).to.be.true - }) - - it('should not be able to lock user-inputted vault with synced key', async () => { - const vault = await context.vaults.createUserInputtedPasswordVault({ - name: 'test vault', - description: 'test vault description', - userInputtedPassword: 'test password', - storagePreference: KeySystemRootKeyStorageMode.Synced, - }) - - expect(() => context.vaultLocks.lockNonPersistentVault(vault)).to.throw( - Error, - 'Vault uses synced key storage and cannot be locked', - ) - }) - - it('should not be able to lock randomized vault', async () => { - const vault = await context.vaults.createRandomizedVault({ - name: 'test vault', - description: 'test vault description', - }) - - expect(() => context.vaultLocks.lockNonPersistentVault(vault)).to.throw( - Error, - 'Vault uses synced key storage and cannot be locked', - ) - }) - - it('should throw if attempting to change password of locked vault', async () => { - const vault = await context.vaults.createUserInputtedPasswordVault({ - name: 'test vault', - description: 'test vault description', - userInputtedPassword: 'test password', - storagePreference: KeySystemRootKeyStorageMode.Ephemeral, - }) - - context.vaultLocks.lockNonPersistentVault(vault) - - await Factory.expectThrowsAsync( - () => context.vaults.changeVaultOptions({ vault }), - 'Attempting to change vault options on a locked vault', - ) - }) - - it('should respect storage preference when rotating key system root key', async () => { - console.error('TODO: implement') - }) - - it('should change storage preference from synced to local', async () => { - console.error('TODO: implement') - }) - - it('should change storage preference from local to synced', async () => { - console.error('TODO: implement') - }) - - it('should resync key system items key if it is encrypted with noncurrent key system root key', async () => { - console.error('TODO: implement') - }) - - it('should change password type from user inputted to randomized', async () => { - console.error('TODO: implement') - }) - - it('should change password type from randomized to user inputted', async () => { - console.error('TODO: implement') - }) - - it('should not be able to change storage mode of third party vault', async () => { - console.error('TODO: implement') - }) -}) diff --git a/packages/web/src/javascripts/Application/Database.ts b/packages/web/src/javascripts/Application/Database.ts index 9482041f5..08938958b 100644 --- a/packages/web/src/javascripts/Application/Database.ts +++ b/packages/web/src/javascripts/Application/Database.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { isString, AlertService, uniqueArray } from '@standardnotes/snjs' const STORE_NAME = 'items' @@ -140,10 +141,6 @@ export class Database { }) } - /** - * This function is actually unused, but implemented to conform to protocol in case it is eventually needed. - * We could remove implementation and throw instead, but it might be better to offer a functional alternative instead. - */ public async getPayloadsForKeys(keys: string[]): Promise { const db = (await this.openDatabase()) as IDBDatabase return new Promise((resolve) => { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx index 2eb60d53f..4d1405c3e 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx @@ -4,7 +4,7 @@ import DecoratedInput from '@/Components/Input/DecoratedInput' import { useApplication } from '@/Components/ApplicationProvider' import { ChangeVaultKeyOptionsDTO, - KeySystemRootKeyPasswordType, + KeySystemPasswordType, KeySystemRootKeyStorageMode, SharedVaultInviteServerHash, SharedVaultUserServerHash, @@ -32,9 +32,7 @@ const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVault const [members, setMembers] = useState([]) const [invites, setInvites] = useState([]) const [isAdmin, setIsAdmin] = useState(true) - const [passwordType, setPasswordType] = useState( - KeySystemRootKeyPasswordType.Randomized, - ) + const [passwordType, setPasswordType] = useState(KeySystemPasswordType.Randomized) const [keyStorageMode, setKeyStorageMode] = useState(KeySystemRootKeyStorageMode.Synced) const [customPassword, setCustomPassword] = useState(undefined) @@ -94,7 +92,7 @@ const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVault throw new Error('Password type is not changing') } - if (passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (passwordType === KeySystemPasswordType.UserInputted) { if (!customPassword) { throw new Error('Custom password is not set') } @@ -113,7 +111,7 @@ const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVault await application.vaults.changeVaultOptions({ vault, newPasswordType: isChangingPasswordType ? getPasswordTypeParams() : undefined, - newKeyStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined, + newStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined, }) } }, @@ -121,7 +119,7 @@ const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVault ) const createNewVault = useCallback(async () => { - if (passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (passwordType === KeySystemPasswordType.UserInputted) { if (!customPassword) { throw new Error('Custom key is not set') } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx index 0b6a7857c..62d719aa3 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx @@ -1,22 +1,22 @@ -import { KeySystemRootKeyPasswordType } from '@standardnotes/snjs' +import { KeySystemPasswordType } from '@standardnotes/snjs' import StyledRadioInput from '@/Components/Radio/StyledRadioInput' import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' import { useState } from 'react' type PasswordTypePreference = { - value: KeySystemRootKeyPasswordType + value: KeySystemPasswordType label: string description: string } const options: PasswordTypePreference[] = [ { - value: KeySystemRootKeyPasswordType.Randomized, + value: KeySystemPasswordType.Randomized, label: 'Randomized (Recommended)', description: 'Your vault key will be randomly generated and synced to your account.', }, { - value: KeySystemRootKeyPasswordType.UserInputted, + value: KeySystemPasswordType.UserInputted, label: 'Custom (Advanced)', description: 'Choose your own key for your vault. This is an advanced option and is not recommended for most users.', @@ -28,8 +28,8 @@ export const PasswordTypePreference = ({ onChange, onCustomKeyChange, }: { - value: KeySystemRootKeyPasswordType - onChange: (value: KeySystemRootKeyPasswordType) => void + value: KeySystemPasswordType + onChange: (value: KeySystemPasswordType) => void onCustomKeyChange: (value: string) => void }) => { const [customKey, setCustomKey] = useState('') @@ -57,7 +57,7 @@ export const PasswordTypePreference = ({ ) })} - {value === KeySystemRootKeyPasswordType.UserInputted && ( + {value === KeySystemPasswordType.UserInputted && (

{options[1].description}