refactor: key rotation (#2383)

This commit is contained in:
Mo
2023-08-04 09:25:28 -05:00
committed by GitHub
parent a7f266bb68
commit 494436bdb6
65 changed files with 1354 additions and 1232 deletions

View File

@@ -1,207 +1,66 @@
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
import {
KeySystemPasswordType,
KeySystemRootKeyStorageMode,
VaultListingInterface,
VaultListingMutator,
} from '@standardnotes/models'
import { RotateVaultKey } from './RotateVaultKey'
import { SyncServiceInterface } from '@standardnotes/services'
import { KeySystemPasswordType, KeySystemRootKeyStorageMode } from '@standardnotes/models'
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'
import { ChangeVaultStorageMode } from './ChangeVaultStorageMode'
export class ChangeVaultKeyOptions implements UseCaseInterface<void> {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface,
private keys: KeySystemKeyManagerInterface,
private getVault: GetVault,
private _rotateVaultKey: RotateVaultKey,
private _changeVaultStorageMode: ChangeVaultStorageMode,
) {}
async execute(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> {
if (dto.newPasswordType) {
const result = await this.handleNewPasswordType(dto)
if (result.isFailed()) {
return result
let newStorageMode = dto.newStorageMode
let vault = dto.vault
if (dto.newPasswordOptions) {
if (
dto.newPasswordOptions.passwordType === KeySystemPasswordType.Randomized &&
dto.newStorageMode &&
dto.newStorageMode !== KeySystemRootKeyStorageMode.Synced
) {
return Result.fail('Cannot change storage mode to non-synced for randomized vault')
}
}
if (dto.newStorageMode) {
const result = await this.handleNewStorageMode(dto)
if (result.isFailed()) {
return result
if (
dto.newPasswordOptions.passwordType === KeySystemPasswordType.UserInputted &&
!dto.newPasswordOptions.userInputtedPassword
) {
return Result.fail('User inputted password required')
}
}
await this.sync.sync()
return Result.ok()
}
private async handleNewPasswordType(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> {
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<Result<void>> {
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<Result<void>> {
const newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
systemIdentifier: vault.systemIdentifier,
userInputtedPassword: userInputtedPassword,
})
if (storageMode === KeySystemRootKeyStorageMode.Synced) {
await this.mutator.insertItem(newRootKey, true)
} else {
this.keys.cacheKey(newRootKey, storageMode)
}
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.rootKeyParams = newRootKey.keyParams
})
await this.keys.queueVaultItemsKeysForReencryption(vault.systemIdentifier)
return Result.ok()
}
private async changePasswordTypeToRandomized(vault: VaultListingInterface): Promise<Result<void>> {
if (vault.keyStorageMode !== KeySystemRootKeyStorageMode.Synced) {
this.keys.removeKeyFromCache(vault.systemIdentifier)
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced
const result = await this._rotateVaultKey.execute({
vault: dto.vault,
userInputtedPassword:
dto.newPasswordOptions.passwordType === KeySystemPasswordType.UserInputted
? dto.newPasswordOptions.userInputtedPassword
: undefined,
})
}
const newRootKey = this.encryption.createRandomizedKeySystemRootKey({
systemIdentifier: vault.systemIdentifier,
})
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.rootKeyParams = newRootKey.keyParams
})
await this.mutator.insertItem(newRootKey, true)
await this.keys.queueVaultItemsKeysForReencryption(vault.systemIdentifier)
return Result.ok()
}
private async changeStorageModeToLocalOrEphemeral(
vault: VaultListingInterface,
newStorageMode: KeySystemRootKeyStorageMode,
): Promise<Result<void>> {
const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier)
if (!primaryKey) {
return Result.fail('No primary key found')
}
if (newStorageMode === KeySystemRootKeyStorageMode.Ephemeral) {
this.keys.removeKeyFromCache(vault.systemIdentifier)
}
this.keys.cacheKey(primaryKey, newStorageMode)
await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier)
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.keyStorageMode = newStorageMode
})
await this.sync.sync()
return Result.ok()
}
private async changeStorageModeToSynced(vault: VaultListingInterface): Promise<Result<void>> {
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) {
continue
if (result.isFailed()) {
return result
}
await this.mutator.insertItem(key)
vault = result.getValue()
if (dto.newPasswordOptions.passwordType === KeySystemPasswordType.Randomized) {
newStorageMode = KeySystemRootKeyStorageMode.Synced
}
}
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced
})
if (newStorageMode && newStorageMode !== vault.keyStorageMode) {
const result = await this._changeVaultStorageMode.execute({
vault: vault,
newStorageMode: newStorageMode,
})
if (result.isFailed()) {
return result
}
}
await this.sync.sync()
return Result.ok()
}

View File

@@ -2,7 +2,7 @@ import { KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterfa
export type ChangeVaultKeyOptionsDTO = {
vault: VaultListingInterface
newPasswordType:
newPasswordOptions:
| { passwordType: KeySystemPasswordType.Randomized }
| { passwordType: KeySystemPasswordType.UserInputted; userInputtedPassword: string }
| undefined

View File

@@ -0,0 +1,107 @@
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
import {
KeySystemPasswordType,
KeySystemRootKeyStorageMode,
VaultListingInterface,
VaultListingMutator,
} from '@standardnotes/models'
import { GetVault } from './GetVault'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class ChangeVaultStorageMode implements UseCaseInterface<VaultListingInterface> {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private keys: KeySystemKeyManagerInterface,
private _getVault: GetVault,
) {}
async execute(dto: {
vault: VaultListingInterface
newStorageMode: KeySystemRootKeyStorageMode
}): Promise<Result<VaultListingInterface>> {
const result = this._getVault.execute({ keySystemIdentifier: dto.vault.systemIdentifier })
if (result.isFailed()) {
return Result.fail('Vault not found')
}
const vault = result.getValue()
if (
vault.keyPasswordType === KeySystemPasswordType.Randomized &&
dto.newStorageMode !== KeySystemRootKeyStorageMode.Synced
) {
return Result.fail('Cannot change storage mode to non-synced for randomized vault')
}
const latestVault = result.getValue()
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 changeStorageModeToLocalOrEphemeral(
vault: VaultListingInterface,
newStorageMode: KeySystemRootKeyStorageMode,
): Promise<Result<VaultListingInterface>> {
const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier)
if (!primaryKey) {
return Result.fail('No primary key found')
}
if (newStorageMode === KeySystemRootKeyStorageMode.Ephemeral) {
this.keys.removeKeyFromCache(vault.systemIdentifier)
}
this.keys.cacheKey(primaryKey, newStorageMode)
await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier)
const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(vault, (mutator) => {
mutator.keyStorageMode = newStorageMode
})
await this.sync.sync()
return Result.ok(updatedVault)
}
private async changeStorageModeToSynced(vault: VaultListingInterface): Promise<Result<VaultListingInterface>> {
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) {
continue
}
await this.mutator.insertItem(key)
}
const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(vault, (mutator) => {
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced
})
return Result.ok(updatedVault)
}
}

View File

@@ -1,9 +1,9 @@
import { IsVaultOwner } from './../../VaultUser/UseCase/IsVaultOwner'
import { NotifyVaultUsersOfKeyRotation } from './../../SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation'
import { UuidGenerator, assert } from '@standardnotes/utils'
import { ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import {
KeySystemIdentifier,
KeySystemRootKeyInterface,
KeySystemPasswordType,
KeySystemRootKeyStorageMode,
VaultListingInterface,
VaultListingMutator,
@@ -11,19 +11,47 @@ import {
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class RotateVaultKey {
export class RotateVaultKey implements UseCaseInterface<VaultListingInterface> {
constructor(
private mutator: MutatorClientInterface,
private encryption: EncryptionProviderInterface,
private keys: KeySystemKeyManagerInterface,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _isVaultOwner: IsVaultOwner,
) {}
async execute(params: {
vault: VaultListingInterface
sharedVaultUuid: string | undefined
userInputtedPassword: string | undefined
}): Promise<undefined | ClientDisplayableError[]> {
}): Promise<Result<VaultListingInterface>> {
const { newRootKey, updatedVault } = await this.updateRootKeyparams(params)
await this.createNewKeySystemItemsKey({
keySystemIdentifier: updatedVault.systemIdentifier,
sharedVaultUuid: updatedVault.isSharedVaultListing() ? updatedVault.sharing.sharedVaultUuid : undefined,
rootKeyToken: newRootKey.token,
})
await this.keys.queueVaultItemsKeysForReencryption(updatedVault.systemIdentifier)
const shareResult = await this.shareNewKeyWithMembers({
vault: updatedVault,
newRootKey,
})
if (shareResult.isFailed()) {
return Result.fail(shareResult.getError())
}
return Result.ok(updatedVault)
}
private async updateRootKeyparams(params: {
vault: VaultListingInterface
userInputtedPassword: string | undefined
}): Promise<{ newRootKey: KeySystemRootKeyInterface; updatedVault: VaultListingInterface }> {
const currentRootKey = this.keys.getPrimaryKeySystemRootKey(params.vault.systemIdentifier)
if (!currentRootKey) {
throw new Error('Cannot rotate key system root key; key system root key not found')
@@ -31,16 +59,12 @@ export class RotateVaultKey {
let newRootKey: KeySystemRootKeyInterface | undefined
if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.UserInputted) {
if (!params.userInputtedPassword) {
throw new Error('Cannot rotate key system root key; user inputted password required')
}
if (params.userInputtedPassword) {
newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
systemIdentifier: params.vault.systemIdentifier,
userInputtedPassword: params.userInputtedPassword,
})
} else if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.Randomized) {
} else {
newRootKey = this.encryption.createRandomizedKeySystemRootKey({
systemIdentifier: params.vault.systemIdentifier,
})
@@ -50,39 +74,28 @@ export class RotateVaultKey {
throw new Error('Cannot rotate key system root key; new root key not created')
}
if (params.vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) {
if (!params.userInputtedPassword || params.vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) {
await this.mutator.insertItem(newRootKey, true)
} else {
this.keys.cacheKey(newRootKey, params.vault.keyStorageMode)
}
await this.mutator.changeItem<VaultListingMutator>(params.vault, (mutator) => {
assert(newRootKey)
mutator.rootKeyParams = newRootKey.keyParams
})
const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(
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.keys.queueVaultItemsKeysForReencryption(params.vault.systemIdentifier)
return errors
return { newRootKey, updatedVault }
}
private async createNewKeySystemItemsKey(params: {
keySystemIdentifier: KeySystemIdentifier
sharedVaultUuid: string | undefined
rootKeyToken: string
}): Promise<ClientDisplayableError | void> {
}): Promise<void> {
const newItemsKeyUuid = UuidGenerator.GenerateUuid()
const newItemsKey = this.encryption.createKeySystemItemsKey(
newItemsKeyUuid,
@@ -92,4 +105,25 @@ export class RotateVaultKey {
)
await this.mutator.insertItem(newItemsKey)
}
private async shareNewKeyWithMembers(params: {
vault: VaultListingInterface
newRootKey: KeySystemRootKeyInterface
}): Promise<Result<void>> {
if (!params.vault.isSharedVaultListing()) {
return Result.ok()
}
const isOwner = this._isVaultOwner.execute({ sharedVault: params.vault }).getValue()
if (!isOwner) {
return Result.ok()
}
const result = await this._notifyVaultUsersOfKeyRotation.execute({
sharedVault: params.vault,
})
return result
}
}

View File

@@ -1,3 +1,4 @@
import { SendVaultDataChangedMessage } from './../SharedVaults/UseCase/SendVaultDataChangedMessage'
import { isClientDisplayableError } from '@standardnotes/responses'
import {
DecryptedItemInterface,
@@ -46,6 +47,7 @@ export class VaultService
private _removeItemFromVault: RemoveItemFromVault,
private _deleteVault: DeleteVault,
private _rotateVaultKey: RotateVaultKey,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
@@ -192,6 +194,12 @@ export class VaultService
await this.sync.sync()
if (updatedVault.isSharedVaultListing()) {
await this._sendVaultDataChangeMessage.execute({
vault: updatedVault,
})
}
return updatedVault
}
@@ -202,12 +210,9 @@ export class VaultService
await this._rotateVaultKey.execute({
vault,
sharedVaultUuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined,
userInputtedPassword: vaultPassword,
})
await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault })
await this.sync.sync()
}
@@ -228,17 +233,13 @@ export class VaultService
return this.getVault({ keySystemIdentifier: latestItem.key_system_identifier })
}
async changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> {
async changeVaultKeyOptions(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> {
if (this.vaultLocks.isVaultLocked(dto.vault)) {
throw new Error('Attempting to change vault options on a locked vault')
}
const result = await this._changeVaultKeyOptions.execute(dto)
if (dto.newPasswordType) {
await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault: dto.vault })
}
return result
}
}

View File

@@ -1,11 +1,3 @@
import { VaultListingInterface } from '@standardnotes/models'
export enum VaultServiceEvent {}
export enum VaultServiceEvent {
VaultRootKeyRotated = 'VaultRootKeyRotated',
}
export type VaultServiceEventPayload = {
[VaultServiceEvent.VaultRootKeyRotated]: {
vault: VaultListingInterface
}
}
export type VaultServiceEventPayload = Record<string, unknown>

View File

@@ -36,5 +36,5 @@ export interface VaultServiceInterface
params: { name: string; description: string },
): Promise<VaultListingInterface>
rotateVaultRootKey(vault: VaultListingInterface, vaultPassword?: string): Promise<void>
changeVaultOptions(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>>
changeVaultKeyOptions(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>>
}