internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
10
packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts
Normal file
10
packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { KeySystemRootKeyPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models'
|
||||
|
||||
export type ChangeVaultOptionsDTO = {
|
||||
vault: VaultListingInterface
|
||||
newPasswordType:
|
||||
| { passwordType: KeySystemRootKeyPasswordType.Randomized }
|
||||
| { passwordType: KeySystemRootKeyPasswordType.UserInputted; userInputtedPassword: string }
|
||||
| undefined
|
||||
newKeyStorageMode: KeySystemRootKeyStorageMode | undefined
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import {
|
||||
KeySystemRootKeyPasswordType,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface, KeySystemKeyManagerInterface } from '@standardnotes/encryption'
|
||||
import { ChangeVaultOptionsDTO } from '../ChangeVaultOptionsDTO'
|
||||
import { GetVaultUseCase } from './GetVault'
|
||||
import { assert } from '@standardnotes/utils'
|
||||
|
||||
export class ChangeVaultKeyOptionsUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
) {}
|
||||
|
||||
private get keys(): KeySystemKeyManagerInterface {
|
||||
return this.encryption.keys
|
||||
}
|
||||
|
||||
async execute(dto: ChangeVaultOptionsDTO): Promise<void> {
|
||||
const useStorageMode = dto.newKeyStorageMode ?? dto.vault.keyStorageMode
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.newKeyStorageMode) {
|
||||
const usecase = new GetVaultUseCase(this.items)
|
||||
const latestVault = usecase.execute({ keySystemIdentifier: dto.vault.systemIdentifier })
|
||||
assert(latestVault)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async changePasswordTypeToUserInputted(
|
||||
vault: VaultListingInterface,
|
||||
userInputtedPassword: string,
|
||||
storageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
const newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
|
||||
systemIdentifier: vault.systemIdentifier,
|
||||
userInputtedPassword: userInputtedPassword,
|
||||
})
|
||||
|
||||
if (storageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
} else {
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, storageMode)
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.rootKeyParams = newRootKey.keyParams
|
||||
})
|
||||
|
||||
await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier)
|
||||
}
|
||||
|
||||
private async changePasswordTypeToRandomized(
|
||||
vault: VaultListingInterface,
|
||||
storageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
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<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.rootKeyParams = newRootKey.keyParams
|
||||
})
|
||||
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
|
||||
await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier)
|
||||
}
|
||||
|
||||
private async changeStorageModeToLocalOrEphemeral(
|
||||
vault: VaultListingInterface,
|
||||
newKeyStorageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier)
|
||||
if (!primaryKey) {
|
||||
throw new Error('No primary key found')
|
||||
}
|
||||
|
||||
this.keys.intakeNonPersistentKeySystemRootKey(primaryKey, newKeyStorageMode)
|
||||
await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier)
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.keyStorageMode = newKeyStorageMode
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async changeStorageModeToSynced(vault: VaultListingInterface): Promise<void> {
|
||||
const allRootKeys = this.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
const syncedRootKeys = this.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
|
||||
for (const key of allRootKeys) {
|
||||
const existingSyncedKey = syncedRootKeys.find((syncedKey) => syncedKey.token === key.token)
|
||||
if (existingSyncedKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.mutator.insertItem(key)
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced
|
||||
})
|
||||
}
|
||||
}
|
||||
115
packages/services/src/Domain/Vaults/UseCase/CreateVault.ts
Normal file
115
packages/services/src/Domain/Vaults/UseCase/CreateVault.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import {
|
||||
KeySystemRootKeyParamsInterface,
|
||||
KeySystemRootKeyPasswordType,
|
||||
VaultListingContentSpecialized,
|
||||
VaultListingInterface,
|
||||
KeySystemRootKeyStorageMode,
|
||||
FillItemContentSpecialized,
|
||||
KeySystemRootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class CreateVaultUseCase {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
const keySystemIdentifier = UuidGenerator.GenerateUuid()
|
||||
|
||||
const rootKey = await this.createKeySystemRootKey({
|
||||
keySystemIdentifier,
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
storagePreference: dto.storagePreference,
|
||||
})
|
||||
|
||||
await this.createKeySystemItemsKey(keySystemIdentifier, rootKey.token)
|
||||
|
||||
const vaultListing = await this.createVaultListing({
|
||||
keySystemIdentifier,
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
passwordType: dto.userInputtedPassword
|
||||
? KeySystemRootKeyPasswordType.UserInputted
|
||||
: KeySystemRootKeyPasswordType.Randomized,
|
||||
rootKeyParams: rootKey.keyParams,
|
||||
storage: dto.storagePreference,
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return vaultListing
|
||||
}
|
||||
|
||||
private async createVaultListing(dto: {
|
||||
keySystemIdentifier: string
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
passwordType: KeySystemRootKeyPasswordType
|
||||
rootKeyParams: KeySystemRootKeyParamsInterface
|
||||
storage: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
const content: VaultListingContentSpecialized = {
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
rootKeyParams: dto.rootKeyParams,
|
||||
keyStorageMode: dto.storage,
|
||||
name: dto.vaultName,
|
||||
description: dto.vaultDescription,
|
||||
}
|
||||
|
||||
return this.mutator.createItem(ContentType.VaultListing, FillItemContentSpecialized(content), true)
|
||||
}
|
||||
|
||||
private async createKeySystemItemsKey(keySystemIdentifier: string, rootKeyToken: string): Promise<void> {
|
||||
const keySystemItemsKey = this.encryption.createKeySystemItemsKey(
|
||||
UuidGenerator.GenerateUuid(),
|
||||
keySystemIdentifier,
|
||||
undefined,
|
||||
rootKeyToken,
|
||||
)
|
||||
|
||||
await this.mutator.insertItem(keySystemItemsKey)
|
||||
}
|
||||
|
||||
private async createKeySystemRootKey(dto: {
|
||||
keySystemIdentifier: string
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<KeySystemRootKeyInterface> {
|
||||
let newRootKey: KeySystemRootKeyInterface | undefined
|
||||
|
||||
if (dto.userInputtedPassword) {
|
||||
newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
})
|
||||
} else {
|
||||
newRootKey = this.encryption.createRandomizedKeySystemRootKey({
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
})
|
||||
}
|
||||
|
||||
if (dto.storagePreference === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
} else {
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, dto.storagePreference)
|
||||
}
|
||||
|
||||
return newRootKey
|
||||
}
|
||||
}
|
||||
32
packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts
Normal file
32
packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { VaultListingInterface } from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class DeleteVaultUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
) {}
|
||||
|
||||
async execute(vault: VaultListingInterface): Promise<ClientDisplayableError | void> {
|
||||
if (!vault.systemIdentifier) {
|
||||
throw new Error('Vault system identifier is missing')
|
||||
}
|
||||
|
||||
await this.encryption.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier)
|
||||
|
||||
const rootKeys = this.encryption.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(rootKeys)
|
||||
|
||||
const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(itemsKeys)
|
||||
|
||||
const vaultItems = this.items.itemsBelongingToKeySystem(vault.systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(vaultItems)
|
||||
|
||||
await this.mutator.setItemToBeDeleted(vault)
|
||||
}
|
||||
}
|
||||
17
packages/services/src/Domain/Vaults/UseCase/GetVault.ts
Normal file
17
packages/services/src/Domain/Vaults/UseCase/GetVault.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { VaultListingInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export class GetVaultUseCase<T extends VaultListingInterface> {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(query: { keySystemIdentifier: string } | { sharedVaultUuid: string }): T | undefined {
|
||||
const vaults = this.items.getItems<VaultListingInterface>(ContentType.VaultListing)
|
||||
|
||||
if ('keySystemIdentifier' in query) {
|
||||
return vaults.find((listing) => listing.systemIdentifier === query.keySystemIdentifier) as T
|
||||
} else {
|
||||
return vaults.find((listing) => listing.sharing?.sharedVaultUuid === query.sharedVaultUuid) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { DecryptedItemInterface, FileItem, VaultListingInterface } from '@standardnotes/models'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export class MoveItemsToVaultUseCase {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
items: DecryptedItemInterface[]
|
||||
vault: VaultListingInterface
|
||||
}): Promise<ClientDisplayableError | void> {
|
||||
for (const item of dto.items) {
|
||||
await this.mutator.changeItem(item, (mutator) => {
|
||||
mutator.key_system_identifier = dto.vault.systemIdentifier
|
||||
mutator.shared_vault_uuid = dto.vault.isSharedVaultListing() ? dto.vault.sharing.sharedVaultUuid : undefined
|
||||
})
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
for (const item of dto.items) {
|
||||
if (item.content_type !== ContentType.File) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (dto.vault.isSharedVaultListing()) {
|
||||
await this.files.moveFileToSharedVault(item as FileItem, dto.vault)
|
||||
} else {
|
||||
const itemPreviouslyBelongedToSharedVault = item.shared_vault_uuid
|
||||
if (itemPreviouslyBelongedToSharedVault) {
|
||||
await this.files.moveFileOutOfSharedVault(item as FileItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { DecryptedItemInterface, FileItem } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
|
||||
export class RemoveItemFromVault {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: { item: DecryptedItemInterface }): Promise<ClientDisplayableError | void> {
|
||||
await this.mutator.changeItem(dto.item, (mutator) => {
|
||||
mutator.key_system_identifier = undefined
|
||||
mutator.shared_vault_uuid = undefined
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
if (dto.item.content_type === ContentType.File) {
|
||||
await this.files.moveFileOutOfSharedVault(dto.item as FileItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { UuidGenerator, assert } from '@standardnotes/utils'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
|
||||
import {
|
||||
KeySystemIdentifier,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyPasswordType,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class RotateVaultRootKeyUseCase {
|
||||
constructor(private mutator: MutatorClientInterface, private encryption: EncryptionProviderInterface) {}
|
||||
|
||||
async execute(params: {
|
||||
vault: VaultListingInterface
|
||||
sharedVaultUuid: string | undefined
|
||||
userInputtedPassword: string | undefined
|
||||
}): Promise<undefined | ClientDisplayableError[]> {
|
||||
const currentRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.vault.systemIdentifier)
|
||||
if (!currentRootKey) {
|
||||
throw new Error('Cannot rotate key system root key; key system root key not found')
|
||||
}
|
||||
|
||||
let newRootKey: KeySystemRootKeyInterface | undefined
|
||||
|
||||
if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.UserInputted) {
|
||||
if (!params.userInputtedPassword) {
|
||||
throw new Error('Cannot rotate key system root key; user inputted password required')
|
||||
}
|
||||
|
||||
newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
|
||||
systemIdentifier: params.vault.systemIdentifier,
|
||||
userInputtedPassword: params.userInputtedPassword,
|
||||
})
|
||||
} else if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.Randomized) {
|
||||
newRootKey = this.encryption.createRandomizedKeySystemRootKey({
|
||||
systemIdentifier: params.vault.systemIdentifier,
|
||||
})
|
||||
}
|
||||
|
||||
if (!newRootKey) {
|
||||
throw new Error('Cannot rotate key system root key; new root key not created')
|
||||
}
|
||||
|
||||
if (params.vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
} else {
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, params.vault.keyStorageMode)
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(params.vault, (mutator) => {
|
||||
assert(newRootKey)
|
||||
mutator.rootKeyParams = newRootKey.keyParams
|
||||
})
|
||||
|
||||
const errors: ClientDisplayableError[] = []
|
||||
|
||||
const updateKeySystemItemsKeyResult = await this.createNewKeySystemItemsKey({
|
||||
keySystemIdentifier: params.vault.systemIdentifier,
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
rootKeyToken: newRootKey.token,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(updateKeySystemItemsKeyResult)) {
|
||||
errors.push(updateKeySystemItemsKeyResult)
|
||||
}
|
||||
|
||||
await this.encryption.reencryptKeySystemItemsKeysForVault(params.vault.systemIdentifier)
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private async createNewKeySystemItemsKey(params: {
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
sharedVaultUuid: string | undefined
|
||||
rootKeyToken: string
|
||||
}): Promise<ClientDisplayableError | void> {
|
||||
const newItemsKeyUuid = UuidGenerator.GenerateUuid()
|
||||
const newItemsKey = this.encryption.createKeySystemItemsKey(
|
||||
newItemsKeyUuid,
|
||||
params.keySystemIdentifier,
|
||||
params.sharedVaultUuid,
|
||||
params.rootKeyToken,
|
||||
)
|
||||
await this.mutator.insertItem(newItemsKey)
|
||||
}
|
||||
}
|
||||
322
packages/services/src/Domain/Vaults/VaultService.ts
Normal file
322
packages/services/src/Domain/Vaults/VaultService.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { isClientDisplayableError } from '@standardnotes/responses'
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
FileItem,
|
||||
KeySystemIdentifier,
|
||||
KeySystemRootKeyPasswordType,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
VaultListingMutator,
|
||||
isNote,
|
||||
} from '@standardnotes/models'
|
||||
import { VaultServiceInterface } from './VaultServiceInterface'
|
||||
import { ChangeVaultOptionsDTO } from './ChangeVaultOptionsDTO'
|
||||
import { VaultServiceEvent, VaultServiceEventPayload } from './VaultServiceEvent'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { CreateVaultUseCase } from './UseCase/CreateVault'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { RemoveItemFromVault } from './UseCase/RemoveItemFromVault'
|
||||
import { DeleteVaultUseCase } from './UseCase/DeleteVault'
|
||||
import { MoveItemsToVaultUseCase } from './UseCase/MoveItemsToVault'
|
||||
|
||||
import { RotateVaultRootKeyUseCase } from './UseCase/RotateVaultRootKey'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { GetVaultUseCase } from './UseCase/GetVault'
|
||||
import { ChangeVaultKeyOptionsUseCase } from './UseCase/ChangeVaultKeyOptions'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
|
||||
export class VaultService
|
||||
extends AbstractService<VaultServiceEvent, VaultServiceEventPayload[VaultServiceEvent]>
|
||||
implements VaultServiceInterface
|
||||
{
|
||||
private lockMap = new Map<VaultListingInterface['uuid'], boolean>()
|
||||
|
||||
constructor(
|
||||
private sync: SyncServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private files: FilesClientInterface,
|
||||
private alerts: AlertService,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
items.addObserver([ContentType.KeySystemItemsKey, ContentType.KeySystemRootKey, ContentType.VaultListing], () => {
|
||||
void this.recomputeAllVaultsLockingState()
|
||||
})
|
||||
}
|
||||
|
||||
getVaults(): VaultListingInterface[] {
|
||||
return this.items.getItems<VaultListingInterface>(ContentType.VaultListing).sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
}
|
||||
|
||||
getLockedvaults(): VaultListingInterface[] {
|
||||
const vaults = this.getVaults()
|
||||
return vaults.filter((vault) => this.isVaultLocked(vault))
|
||||
}
|
||||
|
||||
public getVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface | undefined {
|
||||
const usecase = new GetVaultUseCase(this.items)
|
||||
return usecase.execute(dto)
|
||||
}
|
||||
|
||||
public getSureVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface {
|
||||
const vault = this.getVault(dto)
|
||||
if (!vault) {
|
||||
throw new Error('Vault not found')
|
||||
}
|
||||
|
||||
return vault
|
||||
}
|
||||
|
||||
async createRandomizedVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
return this.createVaultWithParameters({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
userInputtedPassword: undefined,
|
||||
storagePreference: dto.storagePreference,
|
||||
})
|
||||
}
|
||||
|
||||
async createUserInputtedPasswordVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
userInputtedPassword: string
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
return this.createVaultWithParameters(dto)
|
||||
}
|
||||
|
||||
private async createVaultWithParameters(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
const createVault = new CreateVaultUseCase(this.mutator, this.encryption, this.sync)
|
||||
const result = await createVault.execute({
|
||||
vaultName: dto.name,
|
||||
vaultDescription: dto.description,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
storagePreference: dto.storagePreference,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async moveItemToVault(
|
||||
vault: VaultListingInterface,
|
||||
item: DecryptedItemInterface,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
if (this.isVaultLocked(vault)) {
|
||||
throw new Error('Attempting to add item to locked vault')
|
||||
}
|
||||
|
||||
let linkedFiles: FileItem[] = []
|
||||
if (isNote(item)) {
|
||||
linkedFiles = this.items.getNoteLinkedFiles(item)
|
||||
|
||||
if (linkedFiles.length > 0) {
|
||||
const confirmed = await this.alerts.confirmV2({
|
||||
title: 'Linked files will be moved to vault',
|
||||
text: `This note has ${linkedFiles.length} linked files. They will also be moved to the vault. Do you want to continue?`,
|
||||
})
|
||||
if (!confirmed) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useCase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files)
|
||||
await useCase.execute({ vault, items: [item, ...linkedFiles] })
|
||||
|
||||
return this.items.findSureItem(item.uuid)
|
||||
}
|
||||
|
||||
async removeItemFromVault(item: DecryptedItemInterface): Promise<DecryptedItemInterface> {
|
||||
const vault = this.getItemVault(item)
|
||||
if (!vault) {
|
||||
throw new Error('Cannot find vault to remove item from')
|
||||
}
|
||||
|
||||
if (this.isVaultLocked(vault)) {
|
||||
throw new Error('Attempting to remove item from locked vault')
|
||||
}
|
||||
|
||||
const useCase = new RemoveItemFromVault(this.mutator, this.sync, this.files)
|
||||
await useCase.execute({ item })
|
||||
return this.items.findSureItem(item.uuid)
|
||||
}
|
||||
|
||||
async deleteVault(vault: VaultListingInterface): Promise<boolean> {
|
||||
if (vault.isSharedVaultListing()) {
|
||||
throw new Error('Shared vault must be deleted through SharedVaultService')
|
||||
}
|
||||
|
||||
const useCase = new DeleteVaultUseCase(this.items, this.mutator, this.encryption)
|
||||
const error = await useCase.execute(vault)
|
||||
|
||||
if (isClientDisplayableError(error)) {
|
||||
return false
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
return true
|
||||
}
|
||||
|
||||
async changeVaultNameAndDescription(
|
||||
vault: VaultListingInterface,
|
||||
params: { name: string; description?: string },
|
||||
): Promise<VaultListingInterface> {
|
||||
const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(vault, (mutator) => {
|
||||
mutator.name = params.name
|
||||
mutator.description = params.description
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return updatedVault
|
||||
}
|
||||
|
||||
async rotateVaultRootKey(vault: VaultListingInterface): Promise<void> {
|
||||
if (this.computeVaultLockState(vault) === 'locked') {
|
||||
throw new Error('Cannot rotate root key of locked vault')
|
||||
}
|
||||
|
||||
const useCase = new RotateVaultRootKeyUseCase(this.mutator, this.encryption)
|
||||
await useCase.execute({
|
||||
vault,
|
||||
sharedVaultUuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined,
|
||||
userInputtedPassword: undefined,
|
||||
})
|
||||
|
||||
await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault })
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
isItemInVault(item: DecryptedItemInterface): boolean {
|
||||
return item.key_system_identifier !== undefined
|
||||
}
|
||||
|
||||
getItemVault(item: DecryptedItemInterface): VaultListingInterface | undefined {
|
||||
const latestItem = this.items.findItem(item.uuid)
|
||||
if (!latestItem) {
|
||||
throw new Error('Cannot find latest version of item to get vault for')
|
||||
}
|
||||
|
||||
if (!latestItem.key_system_identifier) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.getVault({ keySystemIdentifier: latestItem.key_system_identifier })
|
||||
}
|
||||
|
||||
async changeVaultOptions(dto: ChangeVaultOptionsDTO): Promise<void> {
|
||||
if (this.isVaultLocked(dto.vault)) {
|
||||
throw new Error('Attempting to change vault options on a locked vault')
|
||||
}
|
||||
|
||||
const usecase = new ChangeVaultKeyOptionsUseCase(this.items, this.mutator, this.sync, this.encryption)
|
||||
await usecase.execute(dto)
|
||||
|
||||
if (dto.newPasswordType) {
|
||||
await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault: dto.vault })
|
||||
}
|
||||
}
|
||||
|
||||
public isVaultLocked(vault: VaultListingInterface): boolean {
|
||||
return this.lockMap.get(vault.uuid) === true
|
||||
}
|
||||
|
||||
public async lockNonPersistentVault(vault: VaultListingInterface): Promise<void> {
|
||||
if (vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
throw new Error('Vault uses synced root key and cannot be locked')
|
||||
}
|
||||
|
||||
this.encryption.keys.clearMemoryOfKeysRelatedToVault(vault)
|
||||
|
||||
this.lockMap.set(vault.uuid, true)
|
||||
void this.notifyEventSync(VaultServiceEvent.VaultLocked, { vault })
|
||||
}
|
||||
|
||||
public async unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise<boolean> {
|
||||
if (vault.keyPasswordType !== KeySystemRootKeyPasswordType.UserInputted) {
|
||||
throw new Error('Vault uses randomized password and cannot be unlocked with user inputted password')
|
||||
}
|
||||
|
||||
if (vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
throw new Error('Vault uses synced root key and cannot be unlocked with user inputted password')
|
||||
}
|
||||
|
||||
const derivedRootKey = this.encryption.deriveUserInputtedKeySystemRootKey({
|
||||
keyParams: vault.rootKeyParams,
|
||||
userInputtedPassword: password,
|
||||
})
|
||||
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(derivedRootKey, vault.keyStorageMode)
|
||||
|
||||
await this.encryption.decryptErroredPayloads()
|
||||
|
||||
if (this.computeVaultLockState(vault) === 'locked') {
|
||||
this.encryption.keys.undoIntakeNonPersistentKeySystemRootKey(vault.systemIdentifier)
|
||||
return false
|
||||
}
|
||||
|
||||
this.lockMap.set(vault.uuid, false)
|
||||
void this.notifyEventSync(VaultServiceEvent.VaultUnlocked, { vault })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private recomputeAllVaultsLockingState = async (): Promise<void> => {
|
||||
const vaults = this.getVaults()
|
||||
|
||||
for (const vault of vaults) {
|
||||
const locked = this.computeVaultLockState(vault) === 'locked'
|
||||
|
||||
if (this.lockMap.get(vault.uuid) !== locked) {
|
||||
this.lockMap.set(vault.uuid, locked)
|
||||
|
||||
if (locked) {
|
||||
void this.notifyEvent(VaultServiceEvent.VaultLocked, { vault })
|
||||
} else {
|
||||
void this.notifyEvent(VaultServiceEvent.VaultUnlocked, { vault })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computeVaultLockState(vault: VaultListingInterface): 'locked' | 'unlocked' {
|
||||
const rootKey = this.encryption.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier)
|
||||
if (!rootKey) {
|
||||
return 'locked'
|
||||
}
|
||||
|
||||
const itemsKey = this.encryption.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier)
|
||||
if (!itemsKey) {
|
||||
return 'locked'
|
||||
}
|
||||
|
||||
return 'unlocked'
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
}
|
||||
}
|
||||
19
packages/services/src/Domain/Vaults/VaultServiceEvent.ts
Normal file
19
packages/services/src/Domain/Vaults/VaultServiceEvent.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { VaultListingInterface } from '@standardnotes/models'
|
||||
|
||||
export enum VaultServiceEvent {
|
||||
VaultRootKeyRotated = 'VaultRootKeyRotated',
|
||||
VaultUnlocked = 'VaultUnlocked',
|
||||
VaultLocked = 'VaultLocked',
|
||||
}
|
||||
|
||||
export type VaultServiceEventPayload = {
|
||||
[VaultServiceEvent.VaultRootKeyRotated]: {
|
||||
vault: VaultListingInterface
|
||||
}
|
||||
[VaultServiceEvent.VaultUnlocked]: {
|
||||
vault: VaultListingInterface
|
||||
}
|
||||
[VaultServiceEvent.VaultLocked]: {
|
||||
vault: VaultListingInterface
|
||||
}
|
||||
}
|
||||
47
packages/services/src/Domain/Vaults/VaultServiceInterface.ts
Normal file
47
packages/services/src/Domain/Vaults/VaultServiceInterface.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
KeySystemIdentifier,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { VaultServiceEvent, VaultServiceEventPayload } from './VaultServiceEvent'
|
||||
import { ChangeVaultOptionsDTO } from './ChangeVaultOptionsDTO'
|
||||
|
||||
export interface VaultServiceInterface
|
||||
extends AbstractService<VaultServiceEvent, VaultServiceEventPayload[VaultServiceEvent]> {
|
||||
createRandomizedVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface>
|
||||
createUserInputtedPasswordVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
userInputtedPassword: string
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface>
|
||||
|
||||
getVaults(): VaultListingInterface[]
|
||||
getVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface | undefined
|
||||
getLockedvaults(): VaultListingInterface[]
|
||||
deleteVault(vault: VaultListingInterface): Promise<boolean>
|
||||
|
||||
moveItemToVault(
|
||||
vault: VaultListingInterface,
|
||||
item: DecryptedItemInterface,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
removeItemFromVault(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
|
||||
isItemInVault(item: DecryptedItemInterface): boolean
|
||||
getItemVault(item: DecryptedItemInterface): VaultListingInterface | undefined
|
||||
|
||||
changeVaultNameAndDescription(
|
||||
vault: VaultListingInterface,
|
||||
params: { name: string; description: string },
|
||||
): Promise<VaultListingInterface>
|
||||
rotateVaultRootKey(vault: VaultListingInterface): Promise<void>
|
||||
changeVaultOptions(dto: ChangeVaultOptionsDTO): Promise<void>
|
||||
|
||||
isVaultLocked(vault: VaultListingInterface): boolean
|
||||
unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise<boolean>
|
||||
}
|
||||
Reference in New Issue
Block a user