From 3b531ce8bbcce39bfbcd5dc3f0225948e9fadf97 Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 7 Aug 2023 15:27:06 -0500 Subject: [PATCH] tests: vault locking (#2394) --- .../KeySystemRootKey/KeySystemRootKey.ts | 4 + .../KeySystemRootKeyInterface.ts | 2 + .../src/Domain/Item/ItemManagerInterface.ts | 2 - .../UseCase/ConvertToSharedVault.ts | 10 +- .../SharedVaults/UseCase/CreateSharedVault.ts | 14 +- .../UseCase/GetOwnedSharedVaults.ts | 6 +- .../UseCase/SendVaultDataChangedMessage.ts | 2 +- .../src/Domain/Vault/UseCase/DeleteVault.ts | 6 +- .../src/Domain/Vault/UseCase/GetVaultItems.ts | 11 ++ .../Domain/Vault/UseCase/RotateVaultKey.ts | 2 +- .../services/src/Domain/Vault/VaultService.ts | 36 +++++ .../src/Domain/Vault/VaultServiceInterface.ts | 7 + .../UseCase/ValidateVaultPassword.ts | 30 ++++ .../src/Domain/VaultLock/VaultLockService.ts | 9 ++ .../Domain/VaultUser/UseCase/IsVaultOwner.ts | 14 +- .../src/Domain/VaultUser/VaultUserService.ts | 6 +- packages/services/src/Domain/index.ts | 2 + .../Application/Dependencies/Dependencies.ts | 23 ++- .../lib/Application/Dependencies/Types.ts | 2 + .../snjs/lib/Services/Items/ItemManager.ts | 4 - packages/snjs/mocha/lib/VaultsContext.js | 5 +- packages/snjs/mocha/vaults/invites.test.js | 14 +- .../snjs/mocha/vaults/key-management.test.js | 139 +++++++++++++----- 23 files changed, 268 insertions(+), 82 deletions(-) create mode 100644 packages/services/src/Domain/Vault/UseCase/GetVaultItems.ts create mode 100644 packages/services/src/Domain/VaultLock/UseCase/ValidateVaultPassword.ts diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts index cf81e6baf..39d5893e7 100644 --- a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts @@ -52,4 +52,8 @@ export class KeySystemRootKey extends DecryptedItem imp override get shared_vault_uuid(): undefined { return undefined } + + isEqual(other: KeySystemRootKeyInterface): boolean { + return this.itemsKey === other.itemsKey && this.token === other.token + } } diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts index a65b5d67c..0b5e3ef9f 100644 --- a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts @@ -35,4 +35,6 @@ export interface KeySystemRootKeyInterface extends DecryptedItemInterface(uuids: string[]): (T | undefined)[] get trashedItems(): SNNote[] - itemsBelongingToKeySystem(systemIdentifier: KeySystemIdentifier): DecryptedItemInterface[] hasTagsNeedingFoldersMigration(): boolean get invalidNonVaultedItems(): EncryptedItemInterface[] isTemplateItem(item: DecryptedItemInterface): boolean diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts index 5f89de17b..c75c897c8 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts @@ -1,16 +1,16 @@ +import { GetVaultItems } from './../../Vault/UseCase/GetVaultItems' import { SharedVaultListingInterface, VaultListingInterface, VaultListingMutator } from '@standardnotes/models' import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' import { SharedVaultServerInterface } from '@standardnotes/api' -import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { MoveItemsToVault } from '../../Vault/UseCase/MoveItemsToVault' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' export class ConvertToSharedVault { constructor( - private items: ItemManagerInterface, private mutator: MutatorClientInterface, private sharedVaultServer: SharedVaultServerInterface, - private moveItemsToVault: MoveItemsToVault, + private _moveItemsToVault: MoveItemsToVault, + private _getVaultItems: GetVaultItems, ) {} async execute(dto: { vault: VaultListingInterface }): Promise { @@ -35,9 +35,9 @@ export class ConvertToSharedVault { }, ) - const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier) + const vaultItems = this._getVaultItems.execute(sharedVaultListing).getValue() - await this.moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems }) + await this._moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems }) return sharedVaultListing as SharedVaultListingInterface } diff --git a/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts index 759099675..a40ea8e51 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts @@ -1,3 +1,4 @@ +import { GetVaultItems } from './../../Vault/UseCase/GetVaultItems' import { KeySystemRootKeyStorageMode, SharedVaultListingInterface, @@ -6,18 +7,17 @@ import { } from '@standardnotes/models' import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' import { SharedVaultServerInterface } from '@standardnotes/api' -import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { CreateVault } from '../../Vault/UseCase/CreateVault' import { MoveItemsToVault } from '../../Vault/UseCase/MoveItemsToVault' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' export class CreateSharedVault { constructor( - private items: ItemManagerInterface, private mutator: MutatorClientInterface, private sharedVaultServer: SharedVaultServerInterface, - private createVault: CreateVault, - private moveItemsToVault: MoveItemsToVault, + private _createVault: CreateVault, + private _moveItemsToVault: MoveItemsToVault, + private _getVaultItems: GetVaultItems, ) {} async execute(dto: { @@ -26,7 +26,7 @@ export class CreateSharedVault { userInputtedPassword: string | undefined storagePreference: KeySystemRootKeyStorageMode }): Promise { - const privateVault = await this.createVault.execute({ + const privateVault = await this._createVault.execute({ vaultName: dto.vaultName, vaultDescription: dto.vaultDescription, userInputtedPassword: dto.userInputtedPassword, @@ -50,9 +50,9 @@ export class CreateSharedVault { }, ) - const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier) + const vaultItems = this._getVaultItems.execute(sharedVaultListing).getValue() - await this.moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems }) + await this._moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems }) return sharedVaultListing as SharedVaultListingInterface } diff --git a/packages/services/src/Domain/SharedVaults/UseCase/GetOwnedSharedVaults.ts b/packages/services/src/Domain/SharedVaults/UseCase/GetOwnedSharedVaults.ts index 7f77c6d02..059fad507 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/GetOwnedSharedVaults.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/GetOwnedSharedVaults.ts @@ -13,11 +13,7 @@ export class GetOwnedSharedVaults implements SyncUseCaseInterface { - return this._isVaultOwnwer - .execute({ - sharedVault: vault, - }) - .getValue() + return this._isVaultOwnwer.execute(vault).getValue() }) return Result.ok(ownedVaults) diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts index bfd6b6b59..8d6f4c446 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendVaultDataChangedMessage.ts @@ -28,7 +28,7 @@ export class SendVaultDataChangedMessage implements UseCaseInterface { ) {} async execute(params: { vault: SharedVaultListingInterface }): Promise> { - const isOwner = this._isVaultOwner.execute({ sharedVault: params.vault }).getValue() + const isOwner = this._isVaultOwner.execute(params.vault).getValue() if (!isOwner) { return Result.ok() } diff --git a/packages/services/src/Domain/Vault/UseCase/DeleteVault.ts b/packages/services/src/Domain/Vault/UseCase/DeleteVault.ts index 34638b011..bc3e37ba4 100644 --- a/packages/services/src/Domain/Vault/UseCase/DeleteVault.ts +++ b/packages/services/src/Domain/Vault/UseCase/DeleteVault.ts @@ -1,14 +1,14 @@ import { ClientDisplayableError } from '@standardnotes/responses' -import { ItemManagerInterface } from '../../Item/ItemManagerInterface' import { VaultListingInterface } from '@standardnotes/models' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' +import { GetVaultItems } from './GetVaultItems' export class DeleteVault { constructor( - private items: ItemManagerInterface, private mutator: MutatorClientInterface, private keys: KeySystemKeyManagerInterface, + private _getVaultItems: GetVaultItems, ) {} async execute(vault: VaultListingInterface): Promise { @@ -24,7 +24,7 @@ export class DeleteVault { const itemsKeys = this.keys.getKeySystemItemsKeys(vault.systemIdentifier) await this.mutator.setItemsToBeDeleted(itemsKeys) - const vaultItems = this.items.itemsBelongingToKeySystem(vault.systemIdentifier) + const vaultItems = this._getVaultItems.execute(vault).getValue() await this.mutator.setItemsToBeDeleted(vaultItems) await this.mutator.setItemToBeDeleted(vault) diff --git a/packages/services/src/Domain/Vault/UseCase/GetVaultItems.ts b/packages/services/src/Domain/Vault/UseCase/GetVaultItems.ts new file mode 100644 index 000000000..e53ee4cbb --- /dev/null +++ b/packages/services/src/Domain/Vault/UseCase/GetVaultItems.ts @@ -0,0 +1,11 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { DecryptedItemInterface, ItemContent, VaultListingInterface } from '@standardnotes/models' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' + +export class GetVaultItems implements SyncUseCaseInterface { + constructor(private items: ItemManagerInterface) {} + + execute(vault: VaultListingInterface): Result[]> { + return Result.ok(this.items.items.filter((item) => item.key_system_identifier === vault.systemIdentifier)) + } +} diff --git a/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts b/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts index bf339817f..861868971 100644 --- a/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts +++ b/packages/services/src/Domain/Vault/UseCase/RotateVaultKey.ts @@ -114,7 +114,7 @@ export class RotateVaultKey implements UseCaseInterface { return Result.ok() } - const isOwner = this._isVaultOwner.execute({ sharedVault: params.vault }).getValue() + const isOwner = this._isVaultOwner.execute(params.vault).getValue() if (!isOwner) { return Result.ok() diff --git a/packages/services/src/Domain/Vault/VaultService.ts b/packages/services/src/Domain/Vault/VaultService.ts index 41af05ec8..f24915e5e 100644 --- a/packages/services/src/Domain/Vault/VaultService.ts +++ b/packages/services/src/Domain/Vault/VaultService.ts @@ -1,3 +1,5 @@ +import { ValidateVaultPassword } from './../VaultLock/UseCase/ValidateVaultPassword' +import { IsVaultOwner } from './../VaultUser/UseCase/IsVaultOwner' import { SendVaultDataChangedMessage } from './../SharedVaults/UseCase/SendVaultDataChangedMessage' import { isClientDisplayableError } from '@standardnotes/responses' import { @@ -5,6 +7,7 @@ import { FileItem, KeySystemIdentifier, KeySystemRootKeyStorageMode, + SharedVaultListingInterface, VaultListingInterface, VaultListingMutator, isNote, @@ -48,6 +51,8 @@ export class VaultService private _deleteVault: DeleteVault, private _rotateVaultKey: RotateVaultKey, private _sendVaultDataChangeMessage: SendVaultDataChangedMessage, + private _isVaultOwner: IsVaultOwner, + private _validateVaultPassword: ValidateVaultPassword, eventBus: InternalEventBusInterface, ) { super(eventBus) @@ -238,8 +243,39 @@ export class VaultService throw new Error('Attempting to change vault options on a locked vault') } + if (!this._isVaultOwner.execute(dto.vault).getValue()) { + throw new Error('Third party vault options should be changed via changeThirdPartyVaultStorageOptions') + } + const result = await this._changeVaultKeyOptions.execute(dto) return result } + + async changeThirdPartyVaultStorageOptions(dto: { + vault: SharedVaultListingInterface + newStorageMode: KeySystemRootKeyStorageMode | undefined + vaultPassword: string + }): Promise> { + if (this.vaultLocks.isVaultLocked(dto.vault)) { + throw new Error('Attempting to change vault options on a locked vault') + } + + if (this._isVaultOwner.execute(dto.vault).getValue()) { + throw new Error('First party vault options should be changed via changeVaultKeyOptions') + } + + const validPassword = this._validateVaultPassword.execute(dto.vault, dto.vaultPassword).getValue() + if (!validPassword) { + return Result.fail('Invalid vault password') + } + + const result = await this._changeVaultKeyOptions.execute({ + vault: dto.vault, + newStorageMode: dto.newStorageMode, + newPasswordOptions: undefined, + }) + + return result + } } diff --git a/packages/services/src/Domain/Vault/VaultServiceInterface.ts b/packages/services/src/Domain/Vault/VaultServiceInterface.ts index 184bbb5ed..ec8495c2b 100644 --- a/packages/services/src/Domain/Vault/VaultServiceInterface.ts +++ b/packages/services/src/Domain/Vault/VaultServiceInterface.ts @@ -2,6 +2,7 @@ import { DecryptedItemInterface, KeySystemIdentifier, KeySystemRootKeyStorageMode, + SharedVaultListingInterface, VaultListingInterface, } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' @@ -36,5 +37,11 @@ export interface VaultServiceInterface params: { name: string; description: string }, ): Promise rotateVaultRootKey(vault: VaultListingInterface, vaultPassword?: string): Promise + changeVaultKeyOptions(dto: ChangeVaultKeyOptionsDTO): Promise> + changeThirdPartyVaultStorageOptions(dto: { + vault: SharedVaultListingInterface + newStorageMode: KeySystemRootKeyStorageMode | undefined + vaultPassword: string + }): Promise> } diff --git a/packages/services/src/Domain/VaultLock/UseCase/ValidateVaultPassword.ts b/packages/services/src/Domain/VaultLock/UseCase/ValidateVaultPassword.ts new file mode 100644 index 000000000..b1df69c7d --- /dev/null +++ b/packages/services/src/Domain/VaultLock/UseCase/ValidateVaultPassword.ts @@ -0,0 +1,30 @@ +import { EncryptionProviderInterface } from './../../Encryption/EncryptionProviderInterface' +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { KeySystemPasswordType, VaultListingInterface } from '@standardnotes/models' +import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' + +export class ValidateVaultPassword implements SyncUseCaseInterface { + constructor( + private encryption: EncryptionProviderInterface, + private keys: KeySystemKeyManagerInterface, + ) {} + + execute(vault: VaultListingInterface, password: string): Result { + if (vault.keyPasswordType !== KeySystemPasswordType.UserInputted) { + throw new Error('Vault uses randomized password and cannot be validated with password') + } + + const rootKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier) + + if (!rootKey) { + return Result.ok(false) + } + + const derivedRootKey = this.encryption.deriveUserInputtedKeySystemRootKey({ + keyParams: vault.rootKeyParams, + userInputtedPassword: password, + }) + + return Result.ok(rootKey.isEqual(derivedRootKey)) + } +} diff --git a/packages/services/src/Domain/VaultLock/VaultLockService.ts b/packages/services/src/Domain/VaultLock/VaultLockService.ts index d1e3371f6..d56d53d8e 100644 --- a/packages/services/src/Domain/VaultLock/VaultLockService.ts +++ b/packages/services/src/Domain/VaultLock/VaultLockService.ts @@ -1,3 +1,5 @@ +import { GetVaultItems } from './../Vault/UseCase/GetVaultItems' +import { RemoveItemsFromMemory } from './../Storage/UseCase/RemoveItemsFromMemory' import { GetVaults } from '../Vault/UseCase/GetVaults' import { KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' import { VaultLockServiceInterface } from './VaultLockServiceInterface' @@ -22,6 +24,8 @@ export class VaultLockService private keys: KeySystemKeyManagerInterface, private _getVaults: GetVaults, private _decryptErroredPayloads: DecryptErroredPayloads, + private _removeItemsFromMemory: RemoveItemsFromMemory, + private _getVaultItems: GetVaultItems, eventBus: InternalEventBusInterface, ) { super(eventBus) @@ -40,6 +44,8 @@ export class VaultLockService ;(this.keys as unknown) = undefined ;(this._getVaults as unknown) = undefined ;(this._decryptErroredPayloads as unknown) = undefined + ;(this._removeItemsFromMemory as unknown) = undefined + ;(this._getVaultItems as unknown) = undefined this.lockMap.clear() } @@ -68,6 +74,9 @@ export class VaultLockService await this.keys.wipeVaultKeysFromMemory(vault) + const vaultItems = this._getVaultItems.execute(vault).getValue() + await this._removeItemsFromMemory.execute(vaultItems) + this.lockMap.set(vault.uuid, true) void this.notifyEventSync(VaultLockServiceEvent.VaultLocked, { vault }) diff --git a/packages/services/src/Domain/VaultUser/UseCase/IsVaultOwner.ts b/packages/services/src/Domain/VaultUser/UseCase/IsVaultOwner.ts index 77e615f2a..93bb4e2dc 100644 --- a/packages/services/src/Domain/VaultUser/UseCase/IsVaultOwner.ts +++ b/packages/services/src/Domain/VaultUser/UseCase/IsVaultOwner.ts @@ -1,17 +1,21 @@ import { UserServiceInterface } from './../../User/UserServiceInterface' import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' -import { SharedVaultListingInterface } from '@standardnotes/models' +import { VaultListingInterface } from '@standardnotes/models' export class IsVaultOwner implements SyncUseCaseInterface { constructor(private users: UserServiceInterface) {} - execute(dto: { sharedVault: SharedVaultListingInterface }): Result { - if (!dto.sharedVault.sharing.ownerUserUuid) { - throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) + execute(vault: VaultListingInterface): Result { + if (!vault.sharing) { + return Result.ok(true) + } + + if (!vault.sharing.ownerUserUuid) { + throw new Error(`Shared vault ${vault.sharing.sharedVaultUuid} does not have an owner user uuid`) } const user = this.users.sureUser - return Result.ok(dto.sharedVault.sharing.ownerUserUuid === user.uuid) + return Result.ok(vault.sharing.ownerUserUuid === user.uuid) } } diff --git a/packages/services/src/Domain/VaultUser/VaultUserService.ts b/packages/services/src/Domain/VaultUser/VaultUserService.ts index 993e93e47..3cd2571e8 100644 --- a/packages/services/src/Domain/VaultUser/VaultUserService.ts +++ b/packages/services/src/Domain/VaultUser/VaultUserService.ts @@ -52,11 +52,7 @@ export class VaultUserService extends AbstractService imp } public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean { - return this._isVaultOwner - .execute({ - sharedVault, - }) - .getValue() + return this._isVaultOwner.execute(sharedVault).getValue() } async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise> { diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index f17b8dd9e..158380ae8 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -197,6 +197,7 @@ export * from './Vault/UseCase/ChangeVaultStorageMode' export * from './Vault/UseCase/CreateVault' export * from './Vault/UseCase/DeleteVault' export * from './Vault/UseCase/GetVault' +export * from './Vault/UseCase/GetVaultItems' export * from './Vault/UseCase/GetVaults' export * from './Vault/UseCase/MoveItemsToVault' export * from './Vault/UseCase/RemoveItemFromVault' @@ -214,6 +215,7 @@ export * from './VaultInvite/UseCase/SendVaultInvite' export * from './VaultInvite/VaultInviteService' export * from './VaultInvite/VaultInviteServiceEvent' export * from './VaultInvite/VaultInviteServiceInterface' +export * from './VaultLock/UseCase/ValidateVaultPassword' export * from './VaultLock/VaultLockService' export * from './VaultLock/VaultLockServiceEvent' export * from './VaultLock/VaultLockServiceInterface' diff --git a/packages/snjs/lib/Application/Dependencies/Dependencies.ts b/packages/snjs/lib/Application/Dependencies/Dependencies.ts index e32667c69..45001b572 100644 --- a/packages/snjs/lib/Application/Dependencies/Dependencies.ts +++ b/packages/snjs/lib/Application/Dependencies/Dependencies.ts @@ -131,6 +131,8 @@ import { GetHost, SetHost, GenerateUuid, + GetVaultItems, + ValidateVaultPassword, } from '@standardnotes/services' import { ItemManager } from '../../Services/Items/ItemManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager' @@ -215,10 +217,21 @@ export class Dependencies { } private registerUseCaseMakers() { + this.factory.set(TYPES.ValidateVaultPassword, () => { + return new ValidateVaultPassword( + this.get(TYPES.EncryptionService), + this.get(TYPES.KeySystemKeyManager), + ) + }) + this.factory.set(TYPES.GenerateUuid, () => { return new GenerateUuid(this.get(TYPES.Crypto)) }) + this.factory.set(TYPES.GetVaultItems, () => { + return new GetVaultItems(this.get(TYPES.ItemManager)) + }) + this.factory.set(TYPES.DecryptErroredPayloads, () => { return new DecryptErroredPayloads( this.get(TYPES.ItemsEncryptionService), @@ -389,9 +402,9 @@ export class Dependencies { this.factory.set(TYPES.DeleteVault, () => { return new DeleteVault( - this.get(TYPES.ItemManager), this.get(TYPES.MutatorService), this.get(TYPES.KeySystemKeyManager), + this.get(TYPES.GetVaultItems), ) }) @@ -432,11 +445,11 @@ export class Dependencies { this.factory.set(TYPES.CreateSharedVault, () => { return new CreateSharedVault( - this.get(TYPES.ItemManager), this.get(TYPES.MutatorService), this.get(TYPES.SharedVaultServer), this.get(TYPES.CreateVault), this.get(TYPES.MoveItemsToVault), + this.get(TYPES.GetVaultItems), ) }) @@ -566,10 +579,10 @@ export class Dependencies { this.factory.set(TYPES.ConvertToSharedVault, () => { return new ConvertToSharedVault( - this.get(TYPES.ItemManager), this.get(TYPES.MutatorService), this.get(TYPES.SharedVaultServer), this.get(TYPES.MoveItemsToVault), + this.get(TYPES.GetVaultItems), ) }) @@ -837,6 +850,8 @@ export class Dependencies { this.get(TYPES.KeySystemKeyManager), this.get(TYPES.GetVaults), this.get(TYPES.DecryptErroredPayloads), + this.get(TYPES.RemoveItemsFromMemory), + this.get(TYPES.GetVaultItems), this.get(TYPES.InternalEventBus), ) }) @@ -857,6 +872,8 @@ export class Dependencies { this.get(TYPES.DeleteVault), this.get(TYPES.RotateVaultKey), this.get(TYPES.SendVaultDataChangedMessage), + this.get(TYPES.IsVaultOwner), + this.get(TYPES.ValidateVaultPassword), this.get(TYPES.InternalEventBus), ) }) diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 98c464f10..8b046f291 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -160,6 +160,8 @@ export const TYPES = { GetHost: Symbol.for('GetHost'), SetHost: Symbol.for('SetHost'), GenerateUuid: Symbol.for('GenerateUuid'), + GetVaultItems: Symbol.for('GetVaultItems'), + ValidateVaultPassword: Symbol.for('ValidateVaultPassword'), // Mappers SessionStorageMapper: Symbol.for('SessionStorageMapper'), diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index 8f863d58b..a500f4f47 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -852,10 +852,6 @@ export class ItemManager extends Services.AbstractService implements Services.It : ItemRelationshipDirection.NoRelationship } - itemsBelongingToKeySystem(systemIdentifier: Models.KeySystemIdentifier): Models.DecryptedItemInterface[] { - return this.items.filter((item) => item.key_system_identifier === systemIdentifier) - } - public conflictsOf(uuid: string) { return this.collection.conflictsOf(uuid) } diff --git a/packages/snjs/mocha/lib/VaultsContext.js b/packages/snjs/mocha/lib/VaultsContext.js index 0542a7f10..886c456d2 100644 --- a/packages/snjs/mocha/lib/VaultsContext.js +++ b/packages/snjs/mocha/lib/VaultsContext.js @@ -85,7 +85,6 @@ export class VaultsContext extends AppContext { await this.vaults.moveItemToVault(privateVault, note) const sharedVault = await this.sharedVaults.convertVaultToSharedVault(privateVault) - console.log('createSharedPasswordVault > sharedVault:', sharedVault) const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( this, @@ -94,6 +93,8 @@ export class VaultsContext extends AppContext { await Collaboration.acceptAllInvites(thirdPartyContext) - return { sharedVault, thirdPartyContext, deinitThirdPartyContext } + const contactVault = thirdPartyContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + + return { contactVault, sharedVault, thirdPartyContext, deinitThirdPartyContext } } } diff --git a/packages/snjs/mocha/vaults/invites.test.js b/packages/snjs/mocha/vaults/invites.test.js index f0c784b19..f0c54fb24 100644 --- a/packages/snjs/mocha/vaults/invites.test.js +++ b/packages/snjs/mocha/vaults/invites.test.js @@ -225,6 +225,18 @@ describe('shared vault invites', function () { }) it.skip('should fail to invite user if already member of shared vault', async () => { - console.error('Implement') + const { sharedVault, contact, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite( + context, + ) + + const result = await context.vaultInvites.inviteContactToSharedVault( + sharedVault, + contact, + SharedVaultUserPermission.PERMISSIONS.Write, + ) + + expect(result.isFailed()).to.be.true + + await deinitContactContext() }) }) diff --git a/packages/snjs/mocha/vaults/key-management.test.js b/packages/snjs/mocha/vaults/key-management.test.js index a304f0a53..3ad9ea166 100644 --- a/packages/snjs/mocha/vaults/key-management.test.js +++ b/packages/snjs/mocha/vaults/key-management.test.js @@ -1,4 +1,5 @@ import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -57,7 +58,7 @@ describe('vault key management', function () { await Factory.expectThrowsAsync( () => context.vaults.removeItemFromVault(item), - 'Attempting to remove item from locked vault', + 'Cannot find latest version of item to get vault for', ) }) @@ -117,6 +118,64 @@ describe('vault key management', function () { }) }) + describe('locking 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) + }) + + it('locking should clear vault items from memory', async () => { + const vault = await context.vaults.createUserInputtedPasswordVault({ + name: 'test vault', + description: 'test vault description', + userInputtedPassword: 'test password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const note = await context.createSyncedNote() + await Collaboration.moveItemToVault(context, vault, note) + + await context.vaultLocks.lockNonPersistentVault(vault) + + const decryptedNote = context.items.findItem(note.uuid) + expect(decryptedNote).to.be.undefined + + const encryptedNote = context.items.findAnyItem(note.uuid) + expect(encryptedNote).to.not.be.undefined + expect(isEncryptedItem(encryptedNote)).to.be.true + }) + }) + describe('key rotation and persistence', () => { it('rotating ephemeral vault should not persist keys', async () => { const vault = await context.vaults.createUserInputtedPasswordVault({ @@ -170,43 +229,6 @@ describe('vault key management', function () { }) }) - 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('changeVaultKeyOptions', () => { describe('change storage type', () => { it('should not be able to change randomized vault from synced to local', async () => { @@ -343,6 +365,47 @@ describe('vault key management', function () { const memKeys = context.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) expect(memKeys.length).to.equal(1) }) + + it('should throw if attempting to change key options of third party vault', async () => { + await context.register() + + const { contactVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await Factory.expectThrowsAsync( + () => contactContext.vaults.changeVaultKeyOptions({ vault: contactVault }), + 'Third party vault options should be changed via changeThirdPartyVaultStorageOptions', + ) + + await deinitContactContext() + }) + + it('changing storage options for third party vault should validate password', async () => { + await context.register() + + const { contactVault, thirdPartyContext, deinitThirdPartyContext } = await context.createSharedPasswordVault( + 'test password', + ) + + const invalidResult = await thirdPartyContext.vaults.changeThirdPartyVaultStorageOptions({ + vault: contactVault, + vaultPassword: 'wrong password', + newStorageMode: KeySystemRootKeyStorageMode.Synced, + }) + + expect(invalidResult.isFailed()).to.be.true + expect(invalidResult.getError()).to.equal('Invalid vault password') + + const validResult = await thirdPartyContext.vaults.changeThirdPartyVaultStorageOptions({ + vault: contactVault, + vaultPassword: 'test password', + newStorageMode: KeySystemRootKeyStorageMode.Local, + }) + + expect(validResult.isFailed()).to.be.false + + await deinitThirdPartyContext() + }) }) describe('change password type', () => {