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

@@ -34,7 +34,7 @@ import { StorageValueModes } from '../Storage/StorageTypes'
import { DeinitMode } from './DeinitMode' import { DeinitMode } from './DeinitMode'
import { DeinitSource } from './DeinitSource' import { DeinitSource } from './DeinitSource'
import { UserClientInterface } from '../User/UserClientInterface' import { UserServiceInterface } from '../User/UserServiceInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface'
import { User } from '@standardnotes/responses' import { User } from '@standardnotes/responses'
@@ -113,7 +113,7 @@ export interface ApplicationInterface {
get preferences(): PreferenceServiceInterface get preferences(): PreferenceServiceInterface
get sessions(): SessionsClientInterface get sessions(): SessionsClientInterface
get subscriptions(): SubscriptionManagerInterface get subscriptions(): SubscriptionManagerInterface
get user(): UserClientInterface get user(): UserServiceInterface
get vaults(): VaultServiceInterface get vaults(): VaultServiceInterface
get vaultLocks(): VaultLockServiceInterface get vaultLocks(): VaultLockServiceInterface
get vaultUsers(): VaultUserServiceInterface get vaultUsers(): VaultUserServiceInterface

View File

@@ -1,3 +1,4 @@
import { GetKeyPairs } from './../Encryption/UseCase/GetKeyPairs'
import { GetVault } from './../Vault/UseCase/GetVault' import { GetVault } from './../Vault/UseCase/GetVault'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
@@ -48,6 +49,7 @@ describe('AsymmetricMessageService', () => {
const getOutboundMessagesUseCase = {} as jest.Mocked<GetOutboundMessages> const getOutboundMessagesUseCase = {} as jest.Mocked<GetOutboundMessages>
const getInboundMessagesUseCase = {} as jest.Mocked<GetInboundMessages> const getInboundMessagesUseCase = {} as jest.Mocked<GetInboundMessages>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload> const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
const getKeyPairs = {} as jest.Mocked<GetKeyPairs>
sync = {} as jest.Mocked<SyncServiceInterface> sync = {} as jest.Mocked<SyncServiceInterface>
sync.sync = jest.fn() sync.sync = jest.fn()
@@ -73,6 +75,7 @@ describe('AsymmetricMessageService', () => {
getOutboundMessagesUseCase, getOutboundMessagesUseCase,
getInboundMessagesUseCase, getInboundMessagesUseCase,
getUntrustedPayload, getUntrustedPayload,
getKeyPairs,
eventBus, eventBus,
) )
}) })

View File

@@ -1,3 +1,4 @@
import { GetKeyPairs } from './../Encryption/UseCase/GetKeyPairs'
import { SyncServiceInterface } from './../Sync/SyncServiceInterface' import { SyncServiceInterface } from './../Sync/SyncServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
@@ -54,6 +55,7 @@ export class AsymmetricMessageService
private _getOutboundMessagesUseCase: GetOutboundMessages, private _getOutboundMessagesUseCase: GetOutboundMessages,
private _getInboundMessagesUseCase: GetInboundMessages, private _getInboundMessagesUseCase: GetInboundMessages,
private _getUntrustedPayload: GetUntrustedPayload, private _getUntrustedPayload: GetUntrustedPayload,
private _getKeyPairs: GetKeyPairs,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
@@ -196,8 +198,13 @@ export class AsymmetricMessageService
} }
getUntrustedMessagePayload(message: AsymmetricMessageServerHash): Result<AsymmetricMessagePayload> { getUntrustedMessagePayload(message: AsymmetricMessageServerHash): Result<AsymmetricMessagePayload> {
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail(keys.getError())
}
const result = this._getUntrustedPayload.execute({ const result = this._getUntrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey, privateKey: keys.getValue().encryption.privateKey,
message, message,
}) })
@@ -214,8 +221,13 @@ export class AsymmetricMessageService
return Result.fail(contact.getError()) return Result.fail(contact.getError())
} }
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail(keys.getError())
}
const result = this._getTrustedPayload.execute({ const result = this._getTrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey, privateKey: keys.getValue().encryption.privateKey,
sender: contact.getValue(), sender: contact.getValue(),
ownUserUuid: this.sessions.userUuid, ownUserUuid: this.sessions.userUuid,
message, message,

View File

@@ -11,14 +11,14 @@ import {
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { GetVault } from '../../Vault/UseCase/GetVault' import { GetVault } from '../../Vault/UseCase/GetVault'
import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface' import { DecryptErroredPayloads } from '../../Encryption/UseCase/DecryptErroredPayloads'
export class HandleRootKeyChangedMessage { export class HandleRootKeyChangedMessage {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface, private _getVault: GetVault,
private getVault: GetVault, private _decryptErroredPayloads: DecryptErroredPayloads,
) {} ) {}
async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> { async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> {
@@ -30,14 +30,16 @@ export class HandleRootKeyChangedMessage {
true, true,
) )
const vault = this.getVault.execute<VaultListingInterface>({ keySystemIdentifier: rootKeyContent.systemIdentifier }) const vault = this._getVault.execute<VaultListingInterface>({
keySystemIdentifier: rootKeyContent.systemIdentifier,
})
if (!vault.isFailed()) { if (!vault.isFailed()) {
await this.mutator.changeItem<VaultListingMutator>(vault.getValue(), (mutator) => { await this.mutator.changeItem<VaultListingMutator>(vault.getValue(), (mutator) => {
mutator.rootKeyParams = rootKeyContent.keyParams mutator.rootKeyParams = rootKeyContent.keyParams
}) })
} }
await this.encryption.decryptErroredPayloads() await this._decryptErroredPayloads.execute()
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' }) void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
} }

View File

@@ -16,13 +16,13 @@ export class ProcessAcceptedVaultInvite {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private createOrEditContact: CreateOrEditContact, private _createOrEditContact: CreateOrEditContact,
) {} ) {}
async execute( async execute(
message: AsymmetricMessageSharedVaultInvite, message: AsymmetricMessageSharedVaultInvite,
sharedVaultUuid: string, sharedVaultUuid: string,
senderUuid: string, ownerUuid: string,
): Promise<void> { ): Promise<void> {
const { rootKey: rootKeyContent, trustedContacts, metadata } = message.data const { rootKey: rootKeyContent, trustedContacts, metadata } = message.data
@@ -34,7 +34,7 @@ export class ProcessAcceptedVaultInvite {
description: metadata.description, description: metadata.description,
sharing: { sharing: {
sharedVaultUuid: sharedVaultUuid, sharedVaultUuid: sharedVaultUuid,
ownerUserUuid: senderUuid, ownerUserUuid: ownerUuid,
}, },
} }
@@ -47,7 +47,7 @@ export class ProcessAcceptedVaultInvite {
await this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true) await this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true)
for (const contact of trustedContacts) { for (const contact of trustedContacts) {
await this.createOrEditContact.execute({ await this._createOrEditContact.execute({
name: contact.name, name: contact.name,
contactUuid: contact.contactUuid, contactUuid: contact.contactUuid,
publicKey: contact.publicKeySet.encryption, publicKey: contact.publicKeySet.encryption,

View File

@@ -8,7 +8,7 @@ import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface' import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { UserClientInterface } from '../User/UserClientInterface' import { UserServiceInterface } from '../User/UserServiceInterface'
import { CollaborationIDData, Version1CollaborationId } from './CollaborationID' import { CollaborationIDData, Version1CollaborationId } from './CollaborationID'
import { ValidateItemSigner } from './UseCase/ValidateItemSigner' import { ValidateItemSigner } from './UseCase/ValidateItemSigner'
import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult' import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
@@ -26,7 +26,7 @@ export class ContactService extends AbstractService<ContactServiceEvent> impleme
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,
private user: UserClientInterface, private user: UserServiceInterface,
private selfContactManager: SelfContactManager, private selfContactManager: SelfContactManager,
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private _deleteContact: DeleteContact, private _deleteContact: DeleteContact,

View File

@@ -18,7 +18,7 @@ export class DeleteContact implements UseCaseInterface<void> {
throw new Error('Cannot delete self') throw new Error('Cannot delete self')
} }
const vaults = this.getOwnedVaults.execute({ userUuid: dto.ownUserUuid }) const vaults = this.getOwnedVaults.execute()
if (vaults.isFailed()) { if (vaults.isFailed()) {
return Result.fail('Failed to get owned vaults') return Result.fail('Failed to get owned vaults')
} }

View File

@@ -20,7 +20,6 @@ import {
KeySystemRootKeyParamsInterface, KeySystemRootKeyParamsInterface,
PortablePublicKeySet, PortablePublicKeySet,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export interface EncryptionProviderInterface { export interface EncryptionProviderInterface {
initialize(): Promise<void> initialize(): Promise<void>
@@ -72,7 +71,6 @@ export interface EncryptionProviderInterface {
} }
> >
decryptErroredPayloads(): Promise<void>
deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void> deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void>
unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void> unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void>
@@ -110,9 +108,6 @@ export interface EncryptionProviderInterface {
rootKeyToken: string, rootKeyToken: string,
): KeySystemItemsKeyInterface ): KeySystemItemsKeyInterface
getKeyPair(): PkcKeyPair
getSigningKeyPair(): PkcKeyPair
asymmetricSignatureVerifyDetached( asymmetricSignatureVerifyDetached(
encryptedString: AsymmetricallyEncryptedString, encryptedString: AsymmetricallyEncryptedString,
): AsymmetricSignatureVerificationDetachedResult ): AsymmetricSignatureVerificationDetachedResult

View File

@@ -1,3 +1,4 @@
import { GetKeyPairs } from './UseCase/GetKeyPairs'
import { FindDefaultItemsKey } from './UseCase/ItemsKey/FindDefaultItemsKey' import { FindDefaultItemsKey } from './UseCase/ItemsKey/FindDefaultItemsKey'
import { InternalEventInterface } from './../Internal/InternalEventInterface' import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
@@ -43,7 +44,7 @@ import {
PortablePublicKeySet, PortablePublicKeySet,
RootKeyParamsInterface, RootKeyParamsInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { import {
extendArray, extendArray,
isNotUndefined, isNotUndefined,
@@ -73,7 +74,6 @@ import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/
import { RootKeyManager } from '../RootKeyManager/RootKeyManager' import { RootKeyManager } from '../RootKeyManager/RootKeyManager'
import { RootKeyManagerEvent } from '../RootKeyManager/RootKeyManagerEvent' import { RootKeyManagerEvent } from '../RootKeyManager/RootKeyManagerEvent'
import { CreateNewItemsKeyWithRollback } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback' import { CreateNewItemsKeyWithRollback } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
import { DecryptErroredTypeAPayloads } from './UseCase/TypeA/DecryptErroredPayloads'
import { CreateNewDefaultItemsKey } from './UseCase/ItemsKey/CreateNewDefaultItemsKey' import { CreateNewDefaultItemsKey } from './UseCase/ItemsKey/CreateNewDefaultItemsKey'
import { DecryptTypeAPayload } from './UseCase/TypeA/DecryptPayload' import { DecryptTypeAPayload } from './UseCase/TypeA/DecryptPayload'
import { DecryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/DecryptPayloadWithKeyLookup' import { DecryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/DecryptPayloadWithKeyLookup'
@@ -126,12 +126,12 @@ export class EncryptionService
private crypto: PureCryptoInterface, private crypto: PureCryptoInterface,
private _createNewItemsKeyWithRollback: CreateNewItemsKeyWithRollback, private _createNewItemsKeyWithRollback: CreateNewItemsKeyWithRollback,
private _findDefaultItemsKey: FindDefaultItemsKey, private _findDefaultItemsKey: FindDefaultItemsKey,
private _decryptErroredRootPayloads: DecryptErroredTypeAPayloads,
private _rootKeyEncryptPayloadWithKeyLookup: EncryptTypeAPayloadWithKeyLookup, private _rootKeyEncryptPayloadWithKeyLookup: EncryptTypeAPayloadWithKeyLookup,
private _rootKeyEncryptPayload: EncryptTypeAPayload, private _rootKeyEncryptPayload: EncryptTypeAPayload,
private _rootKeyDecryptPayload: DecryptTypeAPayload, private _rootKeyDecryptPayload: DecryptTypeAPayload,
private _rootKeyDecryptPayloadWithKeyLookup: DecryptTypeAPayloadWithKeyLookup, private _rootKeyDecryptPayloadWithKeyLookup: DecryptTypeAPayloadWithKeyLookup,
private _createDefaultItemsKey: CreateNewDefaultItemsKey, private _createDefaultItemsKey: CreateNewDefaultItemsKey,
private _getKeyPairs: GetKeyPairs,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -157,7 +157,6 @@ export class EncryptionService
;(this.crypto as unknown) = undefined ;(this.crypto as unknown) = undefined
;(this._createNewItemsKeyWithRollback as unknown) = undefined ;(this._createNewItemsKeyWithRollback as unknown) = undefined
;(this._findDefaultItemsKey as unknown) = undefined ;(this._findDefaultItemsKey as unknown) = undefined
;(this._decryptErroredRootPayloads as unknown) = undefined
;(this._rootKeyEncryptPayloadWithKeyLookup as unknown) = undefined ;(this._rootKeyEncryptPayloadWithKeyLookup as unknown) = undefined
;(this._rootKeyEncryptPayload as unknown) = undefined ;(this._rootKeyEncryptPayload as unknown) = undefined
;(this._rootKeyDecryptPayload as unknown) = undefined ;(this._rootKeyDecryptPayload as unknown) = undefined
@@ -167,28 +166,6 @@ export class EncryptionService
super.deinit() super.deinit()
} }
/** @throws */
getKeyPair(): PkcKeyPair {
const rootKey = this.getRootKey()
if (!rootKey?.encryptionKeyPair) {
throw new Error('Account keypair not found')
}
return rootKey.encryptionKeyPair
}
/** @throws */
getSigningKeyPair(): PkcKeyPair {
const rootKey = this.getRootKey()
if (!rootKey?.signingKeyPair) {
throw new Error('Account keypair not found')
}
return rootKey.signingKeyPair
}
hasSigningKeyPair(): boolean { hasSigningKeyPair(): boolean {
return !!this.getRootKey()?.signingKeyPair return !!this.getRootKey()?.signingKeyPair
} }
@@ -244,12 +221,6 @@ export class EncryptionService
return this._createNewItemsKeyWithRollback.execute() return this._createNewItemsKeyWithRollback.execute()
} }
public async decryptErroredPayloads(): Promise<void> {
await this._decryptErroredRootPayloads.execute()
await this.itemsEncryption.decryptErroredItemPayloads()
}
public itemsKeyForEncryptedPayload( public itemsKeyForEncryptedPayload(
payload: EncryptedPayloadInterface, payload: EncryptedPayloadInterface,
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined { ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
@@ -279,7 +250,9 @@ export class EncryptionService
usesKeySystemRootKeyWithKeyLookup, usesKeySystemRootKeyWithKeyLookup,
} = split } = split
const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined const keys = this._getKeyPairs.execute()
const signingKeyPair = keys.isFailed() ? undefined : keys.getValue().signing
if (usesRootKey) { if (usesRootKey) {
const rootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany( const rootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany(

View File

@@ -0,0 +1,18 @@
import { DecryptErroredTypeAPayloads } from './TypeA/DecryptErroredPayloads'
import { ItemsEncryptionService } from './../../ItemsEncryption/ItemsEncryption'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class DecryptErroredPayloads implements UseCaseInterface<void> {
constructor(
private itemsEncryption: ItemsEncryptionService,
private _decryptErroredRootPayloads: DecryptErroredTypeAPayloads,
) {}
async execute(): Promise<Result<void>> {
await this._decryptErroredRootPayloads.execute()
await this.itemsEncryption.decryptErroredItemPayloads()
return Result.ok()
}
}

View File

@@ -0,0 +1,25 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { RootKeyManager } from '../../RootKeyManager/RootKeyManager'
type UsecaseResult = {
encryption: PkcKeyPair
signing: PkcKeyPair
}
export class GetKeyPairs implements SyncUseCaseInterface<UsecaseResult> {
constructor(private rootKeyManager: RootKeyManager) {}
execute(): Result<UsecaseResult> {
const rootKey = this.rootKeyManager.getRootKey()
if (!rootKey?.encryptionKeyPair || !rootKey?.signingKeyPair) {
return Result.fail('Account keypair not found')
}
return Result.ok({
encryption: rootKey.encryptionKeyPair,
signing: rootKey.signingKeyPair,
})
}
}

View File

@@ -1,15 +1,11 @@
import { DiscardItemsLocally } from './../UseCase/DiscardItemsLocally' import { DiscardItemsLocally } from './../UseCase/DiscardItemsLocally'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults' import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
import { IsVaultOwner } from './../VaultUser/UseCase/IsVaultOwner'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault' import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
import { ShareContactWithVault } from './UseCase/ShareContactWithVault' import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { FindContact } from './../Contacts/UseCase/FindContact' import { FindContact } from './../Contacts/UseCase/FindContact'
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
import { HandleKeyPairChange } from './../Contacts/UseCase/HandleKeyPairChange' import { HandleKeyPairChange } from './../Contacts/UseCase/HandleKeyPairChange'
import { CreateSharedVault } from './UseCase/CreateSharedVault' import { CreateSharedVault } from './UseCase/CreateSharedVault'
import { GetVault } from './../Vault/UseCase/GetVault' import { GetVault } from './../Vault/UseCase/GetVault'
@@ -29,20 +25,16 @@ describe('SharedVaultService', () => {
const items = {} as jest.Mocked<ItemManagerInterface> const items = {} as jest.Mocked<ItemManagerInterface>
items.addObserver = jest.fn() items.addObserver = jest.fn()
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
const session = {} as jest.Mocked<SessionsClientInterface> const session = {} as jest.Mocked<SessionsClientInterface>
const getVault = {} as jest.Mocked<GetVault> const getVault = {} as jest.Mocked<GetVault>
const getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults> const getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults>
const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault> const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault>
const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange> const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange>
const notifyVaultUsersOfKeyRotation = {} as jest.Mocked<NotifyVaultUsersOfKeyRotation>
const sendVaultDataChangeMessage = {} as jest.Mocked<SendVaultDataChangedMessage>
const findContact = {} as jest.Mocked<FindContact> const findContact = {} as jest.Mocked<FindContact>
const deleteThirdPartyVault = {} as jest.Mocked<DeleteThirdPartyVault> const deleteThirdPartyVault = {} as jest.Mocked<DeleteThirdPartyVault>
const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault> const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault>
const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault> const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault>
const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault> const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
const isVaultAdmin = {} as jest.Mocked<IsVaultOwner>
const discardItemsLocally = {} as jest.Mocked<DiscardItemsLocally> const discardItemsLocally = {} as jest.Mocked<DiscardItemsLocally>
const eventBus = {} as jest.Mocked<InternalEventBusInterface> const eventBus = {} as jest.Mocked<InternalEventBusInterface>
@@ -50,20 +42,16 @@ describe('SharedVaultService', () => {
service = new SharedVaultService( service = new SharedVaultService(
items, items,
encryption,
session, session,
getVault, getVault,
getOwnedVaults, getOwnedVaults,
createSharedVaultUseCase, createSharedVaultUseCase,
handleKeyPairChange, handleKeyPairChange,
notifyVaultUsersOfKeyRotation,
sendVaultDataChangeMessage,
findContact, findContact,
deleteThirdPartyVault, deleteThirdPartyVault,
shareContactWithVault, shareContactWithVault,
convertToSharedVault, convertToSharedVault,
deleteSharedVaultUseCase, deleteSharedVaultUseCase,
isVaultAdmin,
discardItemsLocally, discardItemsLocally,
eventBus, eventBus,
) )

View File

@@ -22,18 +22,13 @@ import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { NotificationServiceEvent, NotificationServiceEventPayload } from '../UserEvent/NotificationServiceEvent' import { NotificationServiceEvent, NotificationServiceEventPayload } from '../UserEvent/NotificationServiceEvent'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault' import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault' import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { VaultServiceEvent, VaultServiceEventPayload } from '../Vault/VaultServiceEvent'
import { ShareContactWithVault } from './UseCase/ShareContactWithVault' import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
import { CreateSharedVault } from './UseCase/CreateSharedVault' import { CreateSharedVault } from './UseCase/CreateSharedVault'
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault' import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
import { GetVault } from '../Vault/UseCase/GetVault' import { GetVault } from '../Vault/UseCase/GetVault'
import { ContentType, NotificationType, Uuid } from '@standardnotes/domain-core' import { ContentType, NotificationType, Uuid } from '@standardnotes/domain-core'
import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange' import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange'
import { FindContact } from '../Contacts/UseCase/FindContact' import { FindContact } from '../Contacts/UseCase/FindContact'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { IsVaultOwner } from '../VaultUser/UseCase/IsVaultOwner'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults' import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
export class SharedVaultService export class SharedVaultService
@@ -42,20 +37,16 @@ export class SharedVaultService
{ {
constructor( constructor(
private items: ItemManagerInterface, private items: ItemManagerInterface,
private encryption: EncryptionProviderInterface,
private session: SessionsClientInterface, private session: SessionsClientInterface,
private _getVault: GetVault, private _getVault: GetVault,
private _getOwnedSharedVaults: GetOwnedSharedVaults, private _getOwnedSharedVaults: GetOwnedSharedVaults,
private _createSharedVault: CreateSharedVault, private _createSharedVault: CreateSharedVault,
private _handleKeyPairChange: HandleKeyPairChange, private _handleKeyPairChange: HandleKeyPairChange,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
private _findContact: FindContact, private _findContact: FindContact,
private _deleteThirdPartyVault: DeleteThirdPartyVault, private _deleteThirdPartyVault: DeleteThirdPartyVault,
private _shareContactWithVault: ShareContactWithVault, private _shareContactWithVault: ShareContactWithVault,
private _convertToSharedVault: ConvertToSharedVault, private _convertToSharedVault: ConvertToSharedVault,
private _deleteSharedVault: DeleteSharedVault, private _deleteSharedVault: DeleteSharedVault,
private _isVaultAdmin: IsVaultOwner,
private _discardItemsLocally: DiscardItemsLocally, private _discardItemsLocally: DiscardItemsLocally,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
@@ -68,32 +59,20 @@ export class SharedVaultService
} }
}), }),
) )
this.eventDisposers.push(
items.addObserver<VaultListingInterface>(ContentType.TYPES.VaultListing, ({ changed, source }) => {
if (source === PayloadEmitSource.LocalChanged && changed.length > 0) {
void this.handleVaultListingsChange(changed)
}
}),
)
} }
override deinit(): void { override deinit(): void {
super.deinit() super.deinit()
;(this.items as unknown) = undefined ;(this.items as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.session as unknown) = undefined ;(this.session as unknown) = undefined
;(this._getVault as unknown) = undefined ;(this._getVault as unknown) = undefined
;(this._createSharedVault as unknown) = undefined ;(this._createSharedVault as unknown) = undefined
;(this._handleKeyPairChange as unknown) = undefined ;(this._handleKeyPairChange as unknown) = undefined
;(this._notifyVaultUsersOfKeyRotation as unknown) = undefined
;(this._sendVaultDataChangeMessage as unknown) = undefined
;(this._findContact as unknown) = undefined ;(this._findContact as unknown) = undefined
;(this._deleteThirdPartyVault as unknown) = undefined ;(this._deleteThirdPartyVault as unknown) = undefined
;(this._shareContactWithVault as unknown) = undefined ;(this._shareContactWithVault as unknown) = undefined
;(this._convertToSharedVault as unknown) = undefined ;(this._convertToSharedVault as unknown) = undefined
;(this._deleteSharedVault as unknown) = undefined ;(this._deleteSharedVault as unknown) = undefined
;(this._isVaultAdmin as unknown) = undefined
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
@@ -109,11 +88,6 @@ export class SharedVaultService
case NotificationServiceEvent.NotificationReceived: case NotificationServiceEvent.NotificationReceived:
await this.handleUserEvent(event.payload as NotificationServiceEventPayload) await this.handleUserEvent(event.payload as NotificationServiceEventPayload)
break break
case VaultServiceEvent.VaultRootKeyRotated: {
const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated]
await this.handleVaultRootKeyRotatedEvent(payload.vault)
break
}
case SyncEvent.ReceivedRemoteSharedVaults: case SyncEvent.ReceivedRemoteSharedVaults:
void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged)
break break
@@ -141,33 +115,6 @@ export class SharedVaultService
} }
} }
private isCurrentUserVaultOwner(sharedVault: SharedVaultListingInterface): boolean {
if (!sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
}
private async handleVaultRootKeyRotatedEvent(vault: VaultListingInterface): Promise<void> {
if (!vault.isSharedVaultListing()) {
return
}
if (!this.isCurrentUserVaultOwner(vault)) {
return
}
await this._notifyVaultUsersOfKeyRotation.execute({
sharedVault: vault,
senderUuid: this.session.getSureUser().uuid,
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
})
}
async createSharedVault(dto: { async createSharedVault(dto: {
name: string name: string
description?: string description?: string
@@ -198,23 +145,6 @@ export class SharedVaultService
} }
} }
private async handleVaultListingsChange(vaults: VaultListingInterface[]): Promise<void> {
for (const vault of vaults) {
if (!vault.isSharedVaultListing()) {
continue
}
await this._sendVaultDataChangeMessage.execute({
vault,
senderUuid: this.session.getSureUser().uuid,
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
})
}
}
public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> { public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
return this._deleteSharedVault.execute({ sharedVault }) return this._deleteSharedVault.execute({ sharedVault })
} }
@@ -224,17 +154,12 @@ export class SharedVaultService
throw new Error('Cannot share self contact') throw new Error('Cannot share self contact')
} }
const ownedVaults = this._getOwnedSharedVaults.execute({ userUuid: this.session.userUuid }).getValue() const ownedVaults = this._getOwnedSharedVaults.execute().getValue()
for (const vault of ownedVaults) { for (const vault of ownedVaults) {
await this._shareContactWithVault.execute({ await this._shareContactWithVault.execute({
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
sharedVault: vault, sharedVault: vault,
contactToShare: contact, contactToShare: contact,
senderUserUuid: this.session.getSureUser().uuid,
}) })
} }
} }

View File

@@ -5,18 +5,17 @@ import { GetSharedVaults } from './GetSharedVaults'
export class GetOwnedSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> { export class GetOwnedSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> {
constructor( constructor(
private getSharedVaults: GetSharedVaults, private _getSharedVaults: GetSharedVaults,
private isVaultOwnwer: IsVaultOwner, private _isVaultOwnwer: IsVaultOwner,
) {} ) {}
execute(dto: { userUuid: string }): Result<SharedVaultListingInterface[]> { execute(): Result<SharedVaultListingInterface[]> {
const sharedVaults = this.getSharedVaults.execute().getValue() const sharedVaults = this._getSharedVaults.execute().getValue()
const ownedVaults = sharedVaults.filter((vault) => { const ownedVaults = sharedVaults.filter((vault) => {
return this.isVaultOwnwer return this._isVaultOwnwer
.execute({ .execute({
sharedVault: vault, sharedVault: vault,
userUuid: dto.userUuid,
}) })
.getValue() .getValue()
}) })

View File

@@ -2,30 +2,26 @@ import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
import { AsymmetricMessageSharedVaultInvite, SharedVaultListingInterface } from '@standardnotes/models' import { AsymmetricMessageSharedVaultInvite, SharedVaultListingInterface } from '@standardnotes/models'
import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/responses' import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/responses'
import { SendVaultKeyChangedMessage } from './SendVaultKeyChangedMessage' import { SendVaultKeyChangedMessage } from './SendVaultKeyChangedMessage'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { InviteToVault } from '../../VaultInvite/UseCase/InviteToVault' import { InviteToVault } from '../../VaultInvite/UseCase/InviteToVault'
import { GetVaultContacts } from '../../VaultUser/UseCase/GetVaultContacts' import { GetVaultContacts } from '../../VaultUser/UseCase/GetVaultContacts'
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage' import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { FindContact } from '../../Contacts/UseCase/FindContact' import { FindContact } from '../../Contacts/UseCase/FindContact'
import { GetKeyPairs } from '../../Encryption/UseCase/GetKeyPairs'
type Params = { type Params = {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
sharedVault: SharedVaultListingInterface sharedVault: SharedVaultListingInterface
senderUuid: string
} }
export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> { export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
constructor( constructor(
private findContact: FindContact, private _findContact: FindContact,
private sendKeyChangedMessage: SendVaultKeyChangedMessage, private _sendKeyChangedMessage: SendVaultKeyChangedMessage,
private inviteToVault: InviteToVault, private _inviteToVault: InviteToVault,
private inviteServer: SharedVaultInvitesServerInterface, private _inviteServer: SharedVaultInvitesServerInterface,
private getVaultContacts: GetVaultContacts, private _getVaultContacts: GetVaultContacts,
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessageSharedVaultInvite>, private _decryptOwnMessage: DecryptOwnMessage<AsymmetricMessageSharedVaultInvite>,
private _getKeyPairs: GetKeyPairs,
) {} ) {}
async execute(params: Params): Promise<Result<void>> { async execute(params: Params): Promise<Result<void>> {
@@ -44,20 +40,25 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
await this.deleteAllInvites(params.sharedVault.sharing.sharedVaultUuid) await this.deleteAllInvites(params.sharedVault.sharing.sharedVaultUuid)
const contacts = await this.getVaultContacts.execute({ const contacts = await this._getVaultContacts.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false, readFromCache: false,
}) })
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail('Cannot send metadata changed message; keys not found')
}
for (const invite of existingInvites.getValue()) { for (const invite of existingInvites.getValue()) {
const recipient = this.findContact.execute({ userUuid: invite.user_uuid }) const recipient = this._findContact.execute({ userUuid: invite.user_uuid })
if (recipient.isFailed()) { if (recipient.isFailed()) {
continue continue
} }
const decryptedPreviousInvite = this.decryptOwnMessage.execute({ const decryptedPreviousInvite = this._decryptOwnMessage.execute({
message: invite.encrypted_message, message: invite.encrypted_message,
privateKey: params.keys.encryption.privateKey, privateKey: keys.getValue().encryption.privateKey,
recipientPublicKey: recipient.getValue().publicKeySet.encryption, recipientPublicKey: recipient.getValue().publicKeySet.encryption,
}) })
@@ -65,13 +66,11 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
return Result.fail(decryptedPreviousInvite.getError()) return Result.fail(decryptedPreviousInvite.getError())
} }
await this.inviteToVault.execute({ await this._inviteToVault.execute({
keys: params.keys,
sharedVault: params.sharedVault, sharedVault: params.sharedVault,
sharedVaultContacts: !contacts.isFailed() ? contacts.getValue() : [], sharedVaultContacts: !contacts.isFailed() ? contacts.getValue() : [],
recipient: recipient.getValue(), recipient: recipient.getValue(),
permission: invite.permission, permission: invite.permission,
senderUuid: params.senderUuid,
}) })
} }
@@ -79,11 +78,9 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
} }
private async performSendKeyChangeMessage(params: Params): Promise<Result<void>> { private async performSendKeyChangeMessage(params: Params): Promise<Result<void>> {
const result = await this.sendKeyChangedMessage.execute({ const result = await this._sendKeyChangedMessage.execute({
keySystemIdentifier: params.sharedVault.systemIdentifier, keySystemIdentifier: params.sharedVault.systemIdentifier,
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
senderUuid: params.senderUuid,
keys: params.keys,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -94,7 +91,7 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
} }
private async deleteAllInvites(sharedVaultUuid: string): Promise<Result<void>> { private async deleteAllInvites(sharedVaultUuid: string): Promise<Result<void>> {
const response = await this.inviteServer.deleteAllSharedVaultInvites({ const response = await this._inviteServer.deleteAllSharedVaultInvites({
sharedVaultUuid: sharedVaultUuid, sharedVaultUuid: sharedVaultUuid,
}) })
@@ -106,7 +103,7 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
} }
private async getExistingInvites(sharedVaultUuid: string): Promise<Result<SharedVaultInviteServerHash[]>> { private async getExistingInvites(sharedVaultUuid: string): Promise<Result<SharedVaultInviteServerHash[]>> {
const response = await this.inviteServer.getOutboundUserInvites() const response = await this._inviteServer.getOutboundUserInvites()
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
return Result.fail(`Failed to get outbound user invites ${JSON.stringify(response)}`) return Result.fail(`Failed to get outbound user invites ${JSON.stringify(response)}`)

View File

@@ -1,3 +1,5 @@
import { UserServiceInterface } from './../../User/UserServiceInterface'
import { IsVaultOwner } from './../../VaultUser/UseCase/IsVaultOwner'
import { import {
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
AsymmetricMessageSharedVaultMetadataChanged, AsymmetricMessageSharedVaultMetadataChanged,
@@ -12,24 +14,26 @@ import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessa
import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier' import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier'
import { FindContact } from '../../Contacts/UseCase/FindContact' import { FindContact } from '../../Contacts/UseCase/FindContact'
import { GetKeyPairs } from '../../Encryption/UseCase/GetKeyPairs'
export class SendVaultDataChangedMessage implements UseCaseInterface<void> { export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
constructor( constructor(
private encryptMessage: EncryptMessage, private users: UserServiceInterface,
private findContact: FindContact, private _encryptMessage: EncryptMessage,
private getVaultUsers: GetVaultUsers, private _findContact: FindContact,
private sendMessage: SendMessage, private _getVaultUsers: GetVaultUsers,
private _sendMessage: SendMessage,
private _isVaultOwner: IsVaultOwner,
private _getKeyPairs: GetKeyPairs,
) {} ) {}
async execute(params: { async execute(params: { vault: SharedVaultListingInterface }): Promise<Result<void>> {
vault: SharedVaultListingInterface const isOwner = this._isVaultOwner.execute({ sharedVault: params.vault }).getValue()
senderUuid: string if (!isOwner) {
keys: { return Result.ok()
encryption: PkcKeyPair
signing: PkcKeyPair
} }
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ const users = await this._getVaultUsers.execute({
sharedVaultUuid: params.vault.sharing.sharedVaultUuid, sharedVaultUuid: params.vault.sharing.sharedVaultUuid,
readFromCache: false, readFromCache: false,
}) })
@@ -37,20 +41,25 @@ export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
return Result.fail('Cannot send metadata changed message; users not found') return Result.fail('Cannot send metadata changed message; users not found')
} }
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail('Cannot send metadata changed message; keys not found')
}
const errors: string[] = [] const errors: string[] = []
for (const user of users.getValue()) { for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) { if (user.user_uuid === this.users.sureUser.uuid) {
continue continue
} }
const trustedContact = this.findContact.execute({ userUuid: user.user_uuid }) const trustedContact = this._findContact.execute({ userUuid: user.user_uuid })
if (trustedContact.isFailed()) { if (trustedContact.isFailed()) {
continue continue
} }
const sendMessageResult = await this.sendToContact({ const sendMessageResult = await this.sendToContact({
vault: params.vault, vault: params.vault,
keys: params.keys, keys: keys.getValue(),
contact: trustedContact.getValue(), contact: trustedContact.getValue(),
}) })
@@ -84,7 +93,7 @@ export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
}, },
} }
const encryptedMessage = this.encryptMessage.execute({ const encryptedMessage = this._encryptMessage.execute({
message: message, message: message,
keys: params.keys, keys: params.keys,
recipientPublicKey: params.contact.publicKeySet.encryption, recipientPublicKey: params.contact.publicKeySet.encryption,
@@ -100,7 +109,7 @@ export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
params.vault.systemIdentifier, params.vault.systemIdentifier,
) )
const sendMessageResult = await this.sendMessage.execute({ const sendMessageResult = await this._sendMessage.execute({
recipientUuid: params.contact.contactUuid, recipientUuid: params.contact.contactUuid,
encryptedMessage: encryptedMessage.getValue(), encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier, replaceabilityIdentifier,

View File

@@ -1,3 +1,4 @@
import { UserServiceInterface } from './../../User/UserServiceInterface'
import { import {
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
AsymmetricMessageSharedVaultRootKeyChanged, AsymmetricMessageSharedVaultRootKeyChanged,
@@ -13,38 +14,38 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier' import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier'
import { FindContact } from '../../Contacts/UseCase/FindContact' import { FindContact } from '../../Contacts/UseCase/FindContact'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
import { GetKeyPairs } from '../../Encryption/UseCase/GetKeyPairs'
export class SendVaultKeyChangedMessage implements UseCaseInterface<void> { export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
constructor( constructor(
private encryptMessage: EncryptMessage, private users: UserServiceInterface,
private keyManager: KeySystemKeyManagerInterface, private keyManager: KeySystemKeyManagerInterface,
private findContact: FindContact, private _encryptMessage: EncryptMessage,
private sendMessage: SendMessage, private _findContact: FindContact,
private getVaultUsers: GetVaultUsers, private _sendMessage: SendMessage,
private _getVaultUsers: GetVaultUsers,
private _getKeyPairs: GetKeyPairs,
) {} ) {}
async execute(params: { async execute(params: { keySystemIdentifier: KeySystemIdentifier; sharedVaultUuid: string }): Promise<Result<void>> {
keySystemIdentifier: KeySystemIdentifier const users = await this._getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid, readFromCache: false })
sharedVaultUuid: string
senderUuid: string
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid, readFromCache: false })
if (users.isFailed()) { if (users.isFailed()) {
return Result.fail('Cannot send root key changed message; users not found') return Result.fail('Cannot send root key changed message; users not found')
} }
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail('Cannot send root key changed message; keys not found')
}
const errors: string[] = [] const errors: string[] = []
for (const user of users.getValue()) { for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) { if (user.user_uuid === this.users.sureUser.uuid) {
continue continue
} }
const trustedContact = this.findContact.execute({ userUuid: user.user_uuid }) const trustedContact = this._findContact.execute({ userUuid: user.user_uuid })
if (trustedContact.isFailed()) { if (trustedContact.isFailed()) {
continue continue
} }
@@ -52,7 +53,7 @@ export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
const result = await this.sendToContact({ const result = await this.sendToContact({
keySystemIdentifier: params.keySystemIdentifier, keySystemIdentifier: params.keySystemIdentifier,
sharedVaultUuid: params.sharedVaultUuid, sharedVaultUuid: params.sharedVaultUuid,
keys: params.keys, keys: keys.getValue(),
contact: trustedContact.getValue(), contact: trustedContact.getValue(),
}) })
@@ -87,7 +88,7 @@ export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
data: { recipientUuid: params.contact.contactUuid, rootKey: keySystemRootKey.content }, data: { recipientUuid: params.contact.contactUuid, rootKey: keySystemRootKey.content },
} }
const encryptedMessage = this.encryptMessage.execute({ const encryptedMessage = this._encryptMessage.execute({
message: message, message: message,
keys: params.keys, keys: params.keys,
recipientPublicKey: params.contact.publicKeySet.encryption, recipientPublicKey: params.contact.publicKeySet.encryption,
@@ -103,7 +104,7 @@ export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
params.keySystemIdentifier, params.keySystemIdentifier,
) )
const sendMessageResult = await this.sendMessage.execute({ const sendMessageResult = await this._sendMessage.execute({
recipientUuid: params.contact.contactUuid, recipientUuid: params.contact.contactUuid,
encryptedMessage: encryptedMessage.getValue(), encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier, replaceabilityIdentifier,

View File

@@ -1,37 +1,35 @@
import { UserServiceInterface } from './../../User/UserServiceInterface'
import { import {
TrustedContactInterface, TrustedContactInterface,
SharedVaultListingInterface, SharedVaultListingInterface,
AsymmetricMessagePayloadType, AsymmetricMessagePayloadType,
} from '@standardnotes/models' } from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage' import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage' import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core' import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { FindContact } from '../../Contacts/UseCase/FindContact' import { FindContact } from '../../Contacts/UseCase/FindContact'
import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers' import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers'
import { GetKeyPairs } from '../../Encryption/UseCase/GetKeyPairs'
export class ShareContactWithVault implements UseCaseInterface<void> { export class ShareContactWithVault implements UseCaseInterface<void> {
constructor( constructor(
private findContact: FindContact, private users: UserServiceInterface,
private encryptMessage: EncryptMessage, private _findContact: FindContact,
private sendMessage: SendMessage, private _encryptMessage: EncryptMessage,
private getVaultUsers: GetVaultUsers, private _sendMessage: SendMessage,
private _getVaultUsers: GetVaultUsers,
private _getKeyPairs: GetKeyPairs,
) {} ) {}
async execute(params: { async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
senderUserUuid: string
sharedVault: SharedVaultListingInterface sharedVault: SharedVaultListingInterface
contactToShare: TrustedContactInterface contactToShare: TrustedContactInterface
}): Promise<Result<void>> { }): Promise<Result<void>> {
if (params.sharedVault.sharing.ownerUserUuid !== params.senderUserUuid) { if (params.sharedVault.sharing.ownerUserUuid !== this.users.sureUser.uuid) {
return Result.fail('Cannot share contact; user is not the owner of the shared vault') return Result.fail('Cannot share contact; user is not the owner of the shared vault')
} }
const users = await this.getVaultUsers.execute({ const users = await this._getVaultUsers.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false, readFromCache: false,
}) })
@@ -44,8 +42,13 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
return Result.ok() return Result.ok()
} }
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail('Cannot share contact; keys not found')
}
for (const vaultUser of users.getValue()) { for (const vaultUser of users.getValue()) {
if (vaultUser.user_uuid === params.senderUserUuid) { if (vaultUser.user_uuid === this.users.sureUser.uuid) {
continue continue
} }
@@ -53,12 +56,12 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
continue continue
} }
const vaultUserAsContact = this.findContact.execute({ userUuid: vaultUser.user_uuid }) const vaultUserAsContact = this._findContact.execute({ userUuid: vaultUser.user_uuid })
if (vaultUserAsContact.isFailed()) { if (vaultUserAsContact.isFailed()) {
continue continue
} }
const encryptedMessage = this.encryptMessage.execute({ const encryptedMessage = this._encryptMessage.execute({
message: { message: {
type: AsymmetricMessagePayloadType.ContactShare, type: AsymmetricMessagePayloadType.ContactShare,
data: { data: {
@@ -66,7 +69,7 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
trustedContact: params.contactToShare.content, trustedContact: params.contactToShare.content,
}, },
}, },
keys: params.keys, keys: keys.getValue(),
recipientPublicKey: vaultUserAsContact.getValue().publicKeySet.encryption, recipientPublicKey: vaultUserAsContact.getValue().publicKeySet.encryption,
}) })
@@ -74,7 +77,7 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
continue continue
} }
await this.sendMessage.execute({ await this._sendMessage.execute({
recipientUuid: vaultUserAsContact.getValue().contactUuid, recipientUuid: vaultUserAsContact.getValue().contactUuid,
encryptedMessage: encryptedMessage.getValue(), encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: undefined, replaceabilityIdentifier: undefined,

View File

@@ -1,3 +1,4 @@
import { DecryptErroredPayloads } from './../Encryption/UseCase/DecryptErroredPayloads'
import { ReencryptTypeAItems } from './../Encryption/UseCase/TypeA/ReencryptTypeAItems' import { ReencryptTypeAItems } from './../Encryption/UseCase/TypeA/ReencryptTypeAItems'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { UserApiServiceInterface } from '@standardnotes/api' import { UserApiServiceInterface } from '@standardnotes/api'
@@ -27,6 +28,7 @@ describe('UserService', () => {
let protectionService: ProtectionsClientInterface let protectionService: ProtectionsClientInterface
let userApiService: UserApiServiceInterface let userApiService: UserApiServiceInterface
let reencryptTypeAItems: ReencryptTypeAItems let reencryptTypeAItems: ReencryptTypeAItems
let decryptErroredPayloads: DecryptErroredPayloads
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
const createService = () => const createService = () =>
@@ -41,6 +43,7 @@ describe('UserService', () => {
protectionService, protectionService,
userApiService, userApiService,
reencryptTypeAItems, reencryptTypeAItems,
decryptErroredPayloads,
internalEventBus, internalEventBus,
) )

View File

@@ -25,7 +25,7 @@ import {
} from '../Challenge' } from '../Challenge'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { UserClientInterface } from './UserClientInterface' import { UserServiceInterface } from './UserServiceInterface'
import { DeinitSource } from '../Application/DeinitSource' import { DeinitSource } from '../Application/DeinitSource'
import { StoragePersistencePolicies } from '../Storage/StorageTypes' import { StoragePersistencePolicies } from '../Storage/StorageTypes'
import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface'
@@ -38,10 +38,11 @@ import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPay
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse' import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems' import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
import { DecryptErroredPayloads } from '../Encryption/UseCase/DecryptErroredPayloads'
export class UserService export class UserService
extends AbstractService<AccountEvent, AccountEventData> extends AbstractService<AccountEvent, AccountEventData>
implements UserClientInterface, InternalEventHandlerInterface implements UserServiceInterface, InternalEventHandlerInterface
{ {
private signingIn = false private signingIn = false
private registering = false private registering = false
@@ -60,6 +61,7 @@ export class UserService
private protections: ProtectionsClientInterface, private protections: ProtectionsClientInterface,
private userApi: UserApiServiceInterface, private userApi: UserApiServiceInterface,
private _reencryptTypeAItems: ReencryptTypeAItems, private _reencryptTypeAItems: ReencryptTypeAItems,
private _decryptErroredPayloads: DecryptErroredPayloads,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -77,6 +79,7 @@ export class UserService
;(this.protections as unknown) = undefined ;(this.protections as unknown) = undefined
;(this.userApi as unknown) = undefined ;(this.userApi as unknown) = undefined
;(this._reencryptTypeAItems as unknown) = undefined ;(this._reencryptTypeAItems as unknown) = undefined
;(this._decryptErroredPayloads as unknown) = undefined
} }
async handleEvent(event: InternalEventInterface): Promise<void> { async handleEvent(event: InternalEventInterface): Promise<void> {
@@ -104,18 +107,26 @@ export class UserService
}) })
.then(() => { .then(() => {
if (!payload.awaitSync) { if (!payload.awaitSync) {
void this.encryption.decryptErroredPayloads() void this._decryptErroredPayloads.execute()
} }
}) })
if (payload.awaitSync) { if (payload.awaitSync) {
await syncPromise await syncPromise
await this.encryption.decryptErroredPayloads() await this._decryptErroredPayloads.execute()
} }
} }
} }
get user(): User | undefined {
return this.sessions.getUser()
}
get sureUser(): User {
return this.sessions.getSureUser()
}
getUserUuid(): string { getUserUuid(): string {
return this.sessions.userUuid return this.sessions.userUuid
} }

View File

@@ -2,13 +2,15 @@ import { Base64String } from '@standardnotes/sncrypto-common'
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common' import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
import { DeinitSource } from '../Application/DeinitSource' import { DeinitSource } from '../Application/DeinitSource'
import { UserRegistrationResponseBody } from '@standardnotes/api' import { UserRegistrationResponseBody } from '@standardnotes/api'
import { HttpResponse, SignInResponse } from '@standardnotes/responses' import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { AccountEventData } from './AccountEventData' import { AccountEventData } from './AccountEventData'
import { AccountEvent } from './AccountEvent' import { AccountEvent } from './AccountEvent'
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse' import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
export interface UserClientInterface extends AbstractService<AccountEvent, AccountEventData> { export interface UserServiceInterface extends AbstractService<AccountEvent, AccountEventData> {
get user(): User | undefined
get sureUser(): User
getUserUuid(): string getUserUuid(): string
isSignedIn(): boolean isSignedIn(): boolean

View File

@@ -1,207 +1,66 @@
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services' import { RotateVaultKey } from './RotateVaultKey'
import { import { SyncServiceInterface } from '@standardnotes/services'
KeySystemPasswordType, import { KeySystemPasswordType, KeySystemRootKeyStorageMode } from '@standardnotes/models'
KeySystemRootKeyStorageMode,
VaultListingInterface,
VaultListingMutator,
} from '@standardnotes/models'
import { ChangeVaultKeyOptionsDTO } from './ChangeVaultKeyOptionsDTO' 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 { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { ChangeVaultStorageMode } from './ChangeVaultStorageMode'
export class ChangeVaultKeyOptions implements UseCaseInterface<void> { export class ChangeVaultKeyOptions implements UseCaseInterface<void> {
constructor( constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface, private _rotateVaultKey: RotateVaultKey,
private keys: KeySystemKeyManagerInterface, private _changeVaultStorageMode: ChangeVaultStorageMode,
private getVault: GetVault,
) {} ) {}
async execute(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> { async execute(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> {
if (dto.newPasswordType) { let newStorageMode = dto.newStorageMode
const result = await this.handleNewPasswordType(dto) let vault = dto.vault
if (result.isFailed()) {
return result 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) { if (
const result = await this.handleNewStorageMode(dto) dto.newPasswordOptions.passwordType === KeySystemPasswordType.UserInputted &&
if (result.isFailed()) { !dto.newPasswordOptions.userInputtedPassword
return result ) {
return Result.fail('User inputted password required')
} }
}
await this.sync.sync() const result = await this._rotateVaultKey.execute({
vault: dto.vault,
return Result.ok() userInputtedPassword:
} dto.newPasswordOptions.passwordType === KeySystemPasswordType.UserInputted
? dto.newPasswordOptions.userInputtedPassword
private async handleNewPasswordType(dto: ChangeVaultKeyOptionsDTO): Promise<Result<void>> { : undefined,
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
}) })
} if (result.isFailed()) {
return result
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
} }
await this.mutator.insertItem(key) vault = result.getValue()
if (dto.newPasswordOptions.passwordType === KeySystemPasswordType.Randomized) {
newStorageMode = KeySystemRootKeyStorageMode.Synced
}
} }
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => { if (newStorageMode && newStorageMode !== vault.keyStorageMode) {
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced const result = await this._changeVaultStorageMode.execute({
}) vault: vault,
newStorageMode: newStorageMode,
})
if (result.isFailed()) {
return result
}
}
await this.sync.sync()
return Result.ok() return Result.ok()
} }

View File

@@ -2,7 +2,7 @@ import { KeySystemPasswordType, KeySystemRootKeyStorageMode, VaultListingInterfa
export type ChangeVaultKeyOptionsDTO = { export type ChangeVaultKeyOptionsDTO = {
vault: VaultListingInterface vault: VaultListingInterface
newPasswordType: newPasswordOptions:
| { passwordType: KeySystemPasswordType.Randomized } | { passwordType: KeySystemPasswordType.Randomized }
| { passwordType: KeySystemPasswordType.UserInputted; userInputtedPassword: string } | { passwordType: KeySystemPasswordType.UserInputted; userInputtedPassword: string }
| undefined | 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 { UuidGenerator, assert } from '@standardnotes/utils'
import { ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { import {
KeySystemIdentifier, KeySystemIdentifier,
KeySystemRootKeyInterface, KeySystemRootKeyInterface,
KeySystemPasswordType,
KeySystemRootKeyStorageMode, KeySystemRootKeyStorageMode,
VaultListingInterface, VaultListingInterface,
VaultListingMutator, VaultListingMutator,
@@ -11,19 +11,47 @@ import {
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class RotateVaultKey { export class RotateVaultKey implements UseCaseInterface<VaultListingInterface> {
constructor( constructor(
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private keys: KeySystemKeyManagerInterface, private keys: KeySystemKeyManagerInterface,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _isVaultOwner: IsVaultOwner,
) {} ) {}
async execute(params: { async execute(params: {
vault: VaultListingInterface vault: VaultListingInterface
sharedVaultUuid: string | undefined
userInputtedPassword: 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) const currentRootKey = this.keys.getPrimaryKeySystemRootKey(params.vault.systemIdentifier)
if (!currentRootKey) { if (!currentRootKey) {
throw new Error('Cannot rotate key system root key; key system root key not found') 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 let newRootKey: KeySystemRootKeyInterface | undefined
if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.UserInputted) { if (params.userInputtedPassword) {
if (!params.userInputtedPassword) {
throw new Error('Cannot rotate key system root key; user inputted password required')
}
newRootKey = this.encryption.createUserInputtedKeySystemRootKey({ newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
systemIdentifier: params.vault.systemIdentifier, systemIdentifier: params.vault.systemIdentifier,
userInputtedPassword: params.userInputtedPassword, userInputtedPassword: params.userInputtedPassword,
}) })
} else if (currentRootKey.keyParams.passwordType === KeySystemPasswordType.Randomized) { } else {
newRootKey = this.encryption.createRandomizedKeySystemRootKey({ newRootKey = this.encryption.createRandomizedKeySystemRootKey({
systemIdentifier: params.vault.systemIdentifier, 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') 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) await this.mutator.insertItem(newRootKey, true)
} else { } else {
this.keys.cacheKey(newRootKey, params.vault.keyStorageMode) this.keys.cacheKey(newRootKey, params.vault.keyStorageMode)
} }
await this.mutator.changeItem<VaultListingMutator>(params.vault, (mutator) => { const updatedVault = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(
assert(newRootKey) params.vault,
mutator.rootKeyParams = newRootKey.keyParams (mutator) => {
}) assert(newRootKey)
mutator.rootKeyParams = newRootKey.keyParams
},
)
const errors: ClientDisplayableError[] = [] return { newRootKey, updatedVault }
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
} }
private async createNewKeySystemItemsKey(params: { private async createNewKeySystemItemsKey(params: {
keySystemIdentifier: KeySystemIdentifier keySystemIdentifier: KeySystemIdentifier
sharedVaultUuid: string | undefined sharedVaultUuid: string | undefined
rootKeyToken: string rootKeyToken: string
}): Promise<ClientDisplayableError | void> { }): Promise<void> {
const newItemsKeyUuid = UuidGenerator.GenerateUuid() const newItemsKeyUuid = UuidGenerator.GenerateUuid()
const newItemsKey = this.encryption.createKeySystemItemsKey( const newItemsKey = this.encryption.createKeySystemItemsKey(
newItemsKeyUuid, newItemsKeyUuid,
@@ -92,4 +105,25 @@ export class RotateVaultKey {
) )
await this.mutator.insertItem(newItemsKey) 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 { isClientDisplayableError } from '@standardnotes/responses'
import { import {
DecryptedItemInterface, DecryptedItemInterface,
@@ -46,6 +47,7 @@ export class VaultService
private _removeItemFromVault: RemoveItemFromVault, private _removeItemFromVault: RemoveItemFromVault,
private _deleteVault: DeleteVault, private _deleteVault: DeleteVault,
private _rotateVaultKey: RotateVaultKey, private _rotateVaultKey: RotateVaultKey,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
@@ -192,6 +194,12 @@ export class VaultService
await this.sync.sync() await this.sync.sync()
if (updatedVault.isSharedVaultListing()) {
await this._sendVaultDataChangeMessage.execute({
vault: updatedVault,
})
}
return updatedVault return updatedVault
} }
@@ -202,12 +210,9 @@ export class VaultService
await this._rotateVaultKey.execute({ await this._rotateVaultKey.execute({
vault, vault,
sharedVaultUuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined,
userInputtedPassword: vaultPassword, userInputtedPassword: vaultPassword,
}) })
await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault })
await this.sync.sync() await this.sync.sync()
} }
@@ -228,17 +233,13 @@ export class VaultService
return this.getVault({ keySystemIdentifier: latestItem.key_system_identifier }) 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)) { if (this.vaultLocks.isVaultLocked(dto.vault)) {
throw new Error('Attempting to change vault options on a locked vault') throw new Error('Attempting to change vault options on a locked vault')
} }
const result = 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 return result
} }
} }

View File

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

View File

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

View File

@@ -11,27 +11,35 @@ import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessa
import { Result, SharedVaultUserPermission, UseCaseInterface } from '@standardnotes/domain-core' import { Result, SharedVaultUserPermission, UseCaseInterface } from '@standardnotes/domain-core'
import { ShareContactWithVault } from '../../SharedVaults/UseCase/ShareContactWithVault' import { ShareContactWithVault } from '../../SharedVaults/UseCase/ShareContactWithVault'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface' import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
import { GetKeyPairs } from '../../Encryption/UseCase/GetKeyPairs'
export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHash> { export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHash> {
constructor( constructor(
private keyManager: KeySystemKeyManagerInterface, private keyManager: KeySystemKeyManagerInterface,
private encryptMessage: EncryptMessage, private _encryptMessage: EncryptMessage,
private sendInvite: SendVaultInvite, private _sendInvite: SendVaultInvite,
private shareContact: ShareContactWithVault, private _shareContact: ShareContactWithVault,
private _getKeyPairs: GetKeyPairs,
) {} ) {}
async execute(params: { async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
senderUuid: string
sharedVault: SharedVaultListingInterface sharedVault: SharedVaultListingInterface
sharedVaultContacts: TrustedContactInterface[] sharedVaultContacts: TrustedContactInterface[]
recipient: TrustedContactInterface recipient: TrustedContactInterface
permission: string permission: string
}): Promise<Result<SharedVaultInviteServerHash>> { }): Promise<Result<SharedVaultInviteServerHash>> {
const createInviteResult = await this.inviteContact(params) const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return Result.fail('Cannot invite contact; keys not found')
}
const createInviteResult = await this.inviteContact({
keys: keys.getValue(),
sharedVault: params.sharedVault,
sharedVaultContacts: params.sharedVaultContacts,
recipient: params.recipient,
permission: params.permission,
})
if (createInviteResult.isFailed()) { if (createInviteResult.isFailed()) {
return createInviteResult return createInviteResult
@@ -39,8 +47,7 @@ export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHa
await this.shareContactWithOtherVaultMembers({ await this.shareContactWithOtherVaultMembers({
contact: params.recipient, contact: params.recipient,
senderUuid: params.senderUuid, keys: keys.getValue(),
keys: params.keys,
sharedVault: params.sharedVault, sharedVault: params.sharedVault,
}) })
@@ -49,16 +56,13 @@ export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHa
private async shareContactWithOtherVaultMembers(params: { private async shareContactWithOtherVaultMembers(params: {
contact: TrustedContactInterface contact: TrustedContactInterface
senderUuid: string
keys: { keys: {
encryption: PkcKeyPair encryption: PkcKeyPair
signing: PkcKeyPair signing: PkcKeyPair
} }
sharedVault: SharedVaultListingInterface sharedVault: SharedVaultListingInterface
}): Promise<Result<void>> { }): Promise<Result<void>> {
const result = await this.shareContact.execute({ const result = await this._shareContact.execute({
keys: params.keys,
senderUserUuid: params.senderUuid,
sharedVault: params.sharedVault, sharedVault: params.sharedVault,
contactToShare: params.contact, contactToShare: params.contact,
}) })
@@ -108,7 +112,7 @@ export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHa
} }
}) })
const encryptedMessage = this.encryptMessage.execute({ const encryptedMessage = this._encryptMessage.execute({
message: { message: {
type: AsymmetricMessagePayloadType.SharedVaultInvite, type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: { data: {
@@ -129,7 +133,7 @@ export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHa
return Result.fail(encryptedMessage.getError()) return Result.fail(encryptedMessage.getError())
} }
const createInviteResult = await this.sendInvite.execute({ const createInviteResult = await this._sendInvite.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
recipientUuid: params.recipient.contactUuid, recipientUuid: params.recipient.contactUuid,
encryptedMessage: encryptedMessage.getValue(), encryptedMessage: encryptedMessage.getValue(),

View File

@@ -12,7 +12,6 @@ import { GetVault } from '../Vault/UseCase/GetVault'
import { InviteToVault } from './UseCase/InviteToVault' import { InviteToVault } from './UseCase/InviteToVault'
import { GetVaultContacts } from '../VaultUser/UseCase/GetVaultContacts' import { GetVaultContacts } from '../VaultUser/UseCase/GetVaultContacts'
import { SyncServiceInterface } from './../Sync/SyncServiceInterface' import { SyncServiceInterface } from './../Sync/SyncServiceInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts' import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
@@ -33,6 +32,8 @@ import { AbstractService } from './../Service/AbstractService'
import { VaultInviteServiceEvent } from './VaultInviteServiceEvent' import { VaultInviteServiceEvent } from './VaultInviteServiceEvent'
import { ContentType, Result } from '@standardnotes/domain-core' import { ContentType, Result } from '@standardnotes/domain-core'
import { SharedVaultInvitesServer } from '@standardnotes/api' import { SharedVaultInvitesServer } from '@standardnotes/api'
import { GetKeyPairs } from '../Encryption/UseCase/GetKeyPairs'
import { DecryptErroredPayloads } from '../Encryption/UseCase/DecryptErroredPayloads'
export class VaultInviteService export class VaultInviteService
extends AbstractService<VaultInviteServiceEvent> extends AbstractService<VaultInviteServiceEvent>
@@ -45,7 +46,6 @@ export class VaultInviteService
private session: SessionsClientInterface, private session: SessionsClientInterface,
private vaultUsers: VaultUserServiceInterface, private vaultUsers: VaultUserServiceInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface,
private invitesServer: SharedVaultInvitesServer, private invitesServer: SharedVaultInvitesServer,
private _getAllContacts: GetAllContacts, private _getAllContacts: GetAllContacts,
private _getVault: GetVault, private _getVault: GetVault,
@@ -55,6 +55,8 @@ export class VaultInviteService
private _getUntrustedPayload: GetUntrustedPayload, private _getUntrustedPayload: GetUntrustedPayload,
private _findContact: FindContact, private _findContact: FindContact,
private _acceptVaultInvite: AcceptVaultInvite, private _acceptVaultInvite: AcceptVaultInvite,
private _getKeyPairs: GetKeyPairs,
private _decryptErroredPayloads: DecryptErroredPayloads,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
@@ -75,7 +77,6 @@ export class VaultInviteService
;(this.session as unknown) = undefined ;(this.session as unknown) = undefined
;(this.vaultUsers as unknown) = undefined ;(this.vaultUsers as unknown) = undefined
;(this.sync as unknown) = undefined ;(this.sync as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.invitesServer as unknown) = undefined ;(this.invitesServer as unknown) = undefined
;(this._getAllContacts as unknown) = undefined ;(this._getAllContacts as unknown) = undefined
;(this._getVault as unknown) = undefined ;(this._getVault as unknown) = undefined
@@ -85,6 +86,8 @@ export class VaultInviteService
;(this._getUntrustedPayload as unknown) = undefined ;(this._getUntrustedPayload as unknown) = undefined
;(this._findContact as unknown) = undefined ;(this._findContact as unknown) = undefined
;(this._acceptVaultInvite as unknown) = undefined ;(this._acceptVaultInvite as unknown) = undefined
;(this._getKeyPairs as unknown) = undefined
;(this._decryptErroredPayloads as unknown) = undefined
this.pendingInvites = {} this.pendingInvites = {}
} }
@@ -142,7 +145,7 @@ export class VaultInviteService
void this.sync.sync() void this.sync.sync()
await this.encryption.decryptErroredPayloads() await this._decryptErroredPayloads.execute()
await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid]) await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid])
} }
@@ -181,11 +184,6 @@ export class VaultInviteService
const contacts = contactsResult.getValue() const contacts = contactsResult.getValue()
const result = await this._inviteToVault.execute({ const result = await this._inviteToVault.execute({
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
senderUuid: this.session.getSureUser().uuid,
sharedVault, sharedVault,
recipient: contact, recipient: contact,
sharedVaultContacts: contacts, sharedVaultContacts: contacts,
@@ -233,6 +231,11 @@ export class VaultInviteService
return return
} }
const keys = this._getKeyPairs.execute()
if (keys.isFailed()) {
return
}
for (const invite of invites) { for (const invite of invites) {
delete this.pendingInvites[invite.uuid] delete this.pendingInvites[invite.uuid]
@@ -240,7 +243,7 @@ export class VaultInviteService
if (!sender.isFailed()) { if (!sender.isFailed()) {
const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({ const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite, message: invite,
privateKey: this.encryption.getKeyPair().privateKey, privateKey: keys.getValue().encryption.privateKey,
ownUserUuid: this.session.userUuid, ownUserUuid: this.session.userUuid,
sender: sender.getValue(), sender: sender.getValue(),
}) })
@@ -258,7 +261,7 @@ export class VaultInviteService
const untrustedMessage = this._getUntrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({ const untrustedMessage = this._getUntrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite, message: invite,
privateKey: this.encryption.getKeyPair().privateKey, privateKey: keys.getValue().encryption.privateKey,
}) })
if (!untrustedMessage.isFailed()) { if (!untrustedMessage.isFailed()) {

View File

@@ -8,6 +8,7 @@ import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface' import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface' import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
import { DecryptErroredPayloads } from '../Encryption/UseCase/DecryptErroredPayloads'
export class VaultLockService export class VaultLockService
extends AbstractService<VaultLockServiceEvent, VaultLockServiceEventPayload[VaultLockServiceEvent]> extends AbstractService<VaultLockServiceEvent, VaultLockServiceEventPayload[VaultLockServiceEvent]>
@@ -20,6 +21,7 @@ export class VaultLockService
private encryption: EncryptionProviderInterface, private encryption: EncryptionProviderInterface,
private keys: KeySystemKeyManagerInterface, private keys: KeySystemKeyManagerInterface,
private _getVaults: GetVaults, private _getVaults: GetVaults,
private _decryptErroredPayloads: DecryptErroredPayloads,
eventBus: InternalEventBusInterface, eventBus: InternalEventBusInterface,
) { ) {
super(eventBus) super(eventBus)
@@ -37,6 +39,7 @@ export class VaultLockService
;(this.encryption as unknown) = undefined ;(this.encryption as unknown) = undefined
;(this.keys as unknown) = undefined ;(this.keys as unknown) = undefined
;(this._getVaults as unknown) = undefined ;(this._getVaults as unknown) = undefined
;(this._decryptErroredPayloads as unknown) = undefined
this.lockMap.clear() this.lockMap.clear()
} }
@@ -86,7 +89,7 @@ export class VaultLockService
this.keys.cacheKey(derivedRootKey, vault.keyStorageMode) this.keys.cacheKey(derivedRootKey, vault.keyStorageMode)
await this.encryption.decryptErroredPayloads() await this._decryptErroredPayloads.execute()
if (this.computeVaultLockState(vault) === 'locked') { if (this.computeVaultLockState(vault) === 'locked') {
this.keys.removeKeyFromCache(vault.systemIdentifier) this.keys.removeKeyFromCache(vault.systemIdentifier)

View File

@@ -6,12 +6,12 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetVaultContacts implements UseCaseInterface<TrustedContactInterface[]> { export class GetVaultContacts implements UseCaseInterface<TrustedContactInterface[]> {
constructor( constructor(
private findContact: FindContact, private _findContact: FindContact,
private getVaultUsers: GetVaultUsers, private _getVaultUsers: GetVaultUsers,
) {} ) {}
async execute(dto: { sharedVaultUuid: string; readFromCache: boolean }): Promise<Result<TrustedContactInterface[]>> { async execute(dto: { sharedVaultUuid: string; readFromCache: boolean }): Promise<Result<TrustedContactInterface[]>> {
const users = await this.getVaultUsers.execute({ const users = await this._getVaultUsers.execute({
sharedVaultUuid: dto.sharedVaultUuid, sharedVaultUuid: dto.sharedVaultUuid,
readFromCache: dto.readFromCache, readFromCache: dto.readFromCache,
}) })
@@ -21,7 +21,7 @@ export class GetVaultContacts implements UseCaseInterface<TrustedContactInterfac
const contacts = users const contacts = users
.getValue() .getValue()
.map((user) => this.findContact.execute({ userUuid: user.user_uuid })) .map((user) => this._findContact.execute({ userUuid: user.user_uuid }))
.map((result) => (result.isFailed() ? undefined : result.getValue())) .map((result) => (result.isFailed() ? undefined : result.getValue()))
.filter(isNotUndefined) .filter(isNotUndefined)

View File

@@ -1,12 +1,17 @@
import { UserServiceInterface } from './../../User/UserServiceInterface'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models' import { SharedVaultListingInterface } from '@standardnotes/models'
export class IsVaultOwner implements SyncUseCaseInterface<boolean> { export class IsVaultOwner implements SyncUseCaseInterface<boolean> {
execute(dto: { sharedVault: SharedVaultListingInterface; userUuid: string }): Result<boolean> { constructor(private users: UserServiceInterface) {}
execute(dto: { sharedVault: SharedVaultListingInterface }): Result<boolean> {
if (!dto.sharedVault.sharing.ownerUserUuid) { if (!dto.sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
} }
return Result.ok(dto.sharedVault.sharing.ownerUserUuid === dto.userUuid) const user = this.users.sureUser
return Result.ok(dto.sharedVault.sharing.ownerUserUuid === user.uuid)
} }
} }

View File

@@ -1,3 +1,4 @@
import { UserServiceInterface } from './../../User/UserServiceInterface'
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api' import { SharedVaultUsersServerInterface } from '@standardnotes/api'
import { DeleteThirdPartyVault } from '../../SharedVaults/UseCase/DeleteExternalSharedVault' import { DeleteThirdPartyVault } from '../../SharedVaults/UseCase/DeleteExternalSharedVault'
@@ -6,15 +7,13 @@ import { SharedVaultListingInterface } from '@standardnotes/models'
export class LeaveVault { export class LeaveVault {
constructor( constructor(
private users: UserServiceInterface,
private vaultUserServer: SharedVaultUsersServerInterface, private vaultUserServer: SharedVaultUsersServerInterface,
private items: ItemManagerInterface, private items: ItemManagerInterface,
private deleteThirdPartyVault: DeleteThirdPartyVault, private _deleteThirdPartyVault: DeleteThirdPartyVault,
) {} ) {}
async execute(params: { async execute(params: { sharedVault: SharedVaultListingInterface }): Promise<ClientDisplayableError | void> {
sharedVault: SharedVaultListingInterface
userUuid: string
}): Promise<ClientDisplayableError | void> {
const latestVaultListing = this.items.findItem<SharedVaultListingInterface>(params.sharedVault.uuid) const latestVaultListing = this.items.findItem<SharedVaultListingInterface>(params.sharedVault.uuid)
if (!latestVaultListing) { if (!latestVaultListing) {
throw new Error(`LeaveVaultUseCase: Could not find vault ${params.sharedVault.uuid}`) throw new Error(`LeaveVaultUseCase: Could not find vault ${params.sharedVault.uuid}`)
@@ -22,13 +21,13 @@ export class LeaveVault {
const response = await this.vaultUserServer.deleteSharedVaultUser({ const response = await this.vaultUserServer.deleteSharedVaultUser({
sharedVaultUuid: latestVaultListing.sharing.sharedVaultUuid, sharedVaultUuid: latestVaultListing.sharing.sharedVaultUuid,
userUuid: params.userUuid, userUuid: this.users.sureUser.uuid,
}) })
if (isErrorResponse(response)) { if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to leave vault ${JSON.stringify(response)}`) return ClientDisplayableError.FromString(`Failed to leave vault ${JSON.stringify(response)}`)
} }
await this.deleteThirdPartyVault.execute(latestVaultListing) await this._deleteThirdPartyVault.execute(latestVaultListing)
} }
} }

View File

@@ -4,7 +4,6 @@ import { GetVault } from '../Vault/UseCase/GetVault'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember' import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
import { VaultServiceInterface } from '../Vault/VaultServiceInterface' import { VaultServiceInterface } from '../Vault/VaultServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { GetVaultUsers } from './UseCase/GetVaultUsers' import { GetVaultUsers } from './UseCase/GetVaultUsers'
import { SharedVaultListingInterface } from '@standardnotes/models' import { SharedVaultListingInterface } from '@standardnotes/models'
import { VaultUserServiceInterface } from './VaultUserServiceInterface' import { VaultUserServiceInterface } from './VaultUserServiceInterface'
@@ -16,7 +15,6 @@ import { IsVaultOwner } from './UseCase/IsVaultOwner'
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface { export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor( constructor(
private session: SessionsClientInterface,
private vaults: VaultServiceInterface, private vaults: VaultServiceInterface,
private vaultLocks: VaultLockServiceInterface, private vaultLocks: VaultLockServiceInterface,
private _getVaultUsers: GetVaultUsers, private _getVaultUsers: GetVaultUsers,
@@ -31,7 +29,6 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
override deinit(): void { override deinit(): void {
super.deinit() super.deinit()
;(this.session as unknown) = undefined
;(this.vaults as unknown) = undefined ;(this.vaults as unknown) = undefined
;(this._getVaultUsers as unknown) = undefined ;(this._getVaultUsers as unknown) = undefined
;(this._removeVaultMember as unknown) = undefined ;(this._removeVaultMember as unknown) = undefined
@@ -58,7 +55,6 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
return this._isVaultOwner return this._isVaultOwner
.execute({ .execute({
sharedVault, sharedVault,
userUuid: this.session.userUuid,
}) })
.getValue() .getValue()
} }
@@ -101,7 +97,6 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> { async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
const result = await this._leaveVault.execute({ const result = await this._leaveVault.execute({
sharedVault: sharedVault, sharedVault: sharedVault,
userUuid: this.session.getSureUser().uuid,
}) })
if (isClientDisplayableError(result)) { if (isClientDisplayableError(result)) {

View File

@@ -21,7 +21,6 @@ export * from './AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite'
export * from './AsymmetricMessage/UseCase/ResendAllMessages' export * from './AsymmetricMessage/UseCase/ResendAllMessages'
export * from './AsymmetricMessage/UseCase/ResendMessage' export * from './AsymmetricMessage/UseCase/ResendMessage'
export * from './AsymmetricMessage/UseCase/SendMessage' export * from './AsymmetricMessage/UseCase/SendMessage'
export * from './Contacts/UseCase/SendOwnContactChangeMessage'
export * from './Auth/AuthClientInterface' export * from './Auth/AuthClientInterface'
export * from './Auth/AuthManager' export * from './Auth/AuthManager'
export * from './Authenticator/AuthenticatorClientInterface' export * from './Authenticator/AuthenticatorClientInterface'
@@ -42,6 +41,7 @@ export * from './Contacts/UseCase/FindContact'
export * from './Contacts/UseCase/GetAllContacts' export * from './Contacts/UseCase/GetAllContacts'
export * from './Contacts/UseCase/HandleKeyPairChange' export * from './Contacts/UseCase/HandleKeyPairChange'
export * from './Contacts/UseCase/ReplaceContactData' export * from './Contacts/UseCase/ReplaceContactData'
export * from './Contacts/UseCase/SendOwnContactChangeMessage'
export * from './Contacts/UseCase/Types/ItemSignatureValidationResult' export * from './Contacts/UseCase/Types/ItemSignatureValidationResult'
export * from './Contacts/UseCase/ValidateItemSigner' export * from './Contacts/UseCase/ValidateItemSigner'
export * from './Device/DatabaseItemMetadata' export * from './Device/DatabaseItemMetadata'
@@ -67,6 +67,8 @@ export * from './Encryption/UseCase/Asymmetric/DecryptOwnMessage'
export * from './Encryption/UseCase/Asymmetric/EncryptMessage' export * from './Encryption/UseCase/Asymmetric/EncryptMessage'
export * from './Encryption/UseCase/Asymmetric/GetMessageAdditionalData' export * from './Encryption/UseCase/Asymmetric/GetMessageAdditionalData'
export * from './Encryption/UseCase/DecryptBackupFile' export * from './Encryption/UseCase/DecryptBackupFile'
export * from './Encryption/UseCase/DecryptErroredPayloads'
export * from './Encryption/UseCase/GetKeyPairs'
export * from './Encryption/UseCase/ItemsKey/CreateNewDefaultItemsKey' export * from './Encryption/UseCase/ItemsKey/CreateNewDefaultItemsKey'
export * from './Encryption/UseCase/ItemsKey/CreateNewItemsKeyWithRollback' export * from './Encryption/UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
export * from './Encryption/UseCase/ItemsKey/FindDefaultItemsKey' export * from './Encryption/UseCase/ItemsKey/FindDefaultItemsKey'
@@ -174,23 +176,14 @@ export * from './User/AccountEventData'
export * from './User/CredentialsChangeFunctionResponse' export * from './User/CredentialsChangeFunctionResponse'
export * from './User/SignedInOrRegisteredEventPayload' export * from './User/SignedInOrRegisteredEventPayload'
export * from './User/SignedOutEventPayload' export * from './User/SignedOutEventPayload'
export * from './User/UserClientInterface' export * from './User/UserServiceInterface'
export * from './User/UserClientInterface' export * from './User/UserServiceInterface'
export * from './User/UserService' export * from './User/UserService'
export * from './UserEvent/NotificationService' export * from './UserEvent/NotificationService'
export * from './UserEvent/NotificationServiceEvent' export * from './UserEvent/NotificationServiceEvent'
export * from './VaultInvite/InviteRecord' export * from './Vault/UseCase/ChangeVaultStorageMode'
export * from './VaultInvite/UseCase/AcceptVaultInvite'
export * from './VaultInvite/UseCase/InviteToVault'
export * from './VaultInvite/UseCase/ReuploadAllInvites'
export * from './VaultInvite/UseCase/ReuploadInvite'
export * from './VaultInvite/UseCase/ReuploadVaultInvites'
export * from './VaultInvite/UseCase/SendVaultInvite'
export * from './VaultInvite/VaultInviteService'
export * from './VaultInvite/VaultInviteServiceEvent'
export * from './VaultInvite/VaultInviteServiceInterface'
export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO'
export * from './Vault/UseCase/ChangeVaultKeyOptions' export * from './Vault/UseCase/ChangeVaultKeyOptions'
export * from './Vault/UseCase/ChangeVaultKeyOptionsDTO'
export * from './Vault/UseCase/CreateVault' export * from './Vault/UseCase/CreateVault'
export * from './Vault/UseCase/DeleteVault' export * from './Vault/UseCase/DeleteVault'
export * from './Vault/UseCase/GetVault' export * from './Vault/UseCase/GetVault'
@@ -201,6 +194,16 @@ export * from './Vault/UseCase/RotateVaultKey'
export * from './Vault/VaultService' export * from './Vault/VaultService'
export * from './Vault/VaultServiceEvent' export * from './Vault/VaultServiceEvent'
export * from './Vault/VaultServiceInterface' export * from './Vault/VaultServiceInterface'
export * from './VaultInvite/InviteRecord'
export * from './VaultInvite/UseCase/AcceptVaultInvite'
export * from './VaultInvite/UseCase/InviteToVault'
export * from './VaultInvite/UseCase/ReuploadAllInvites'
export * from './VaultInvite/UseCase/ReuploadInvite'
export * from './VaultInvite/UseCase/ReuploadVaultInvites'
export * from './VaultInvite/UseCase/SendVaultInvite'
export * from './VaultInvite/VaultInviteService'
export * from './VaultInvite/VaultInviteServiceEvent'
export * from './VaultInvite/VaultInviteServiceInterface'
export * from './VaultLock/VaultLockService' export * from './VaultLock/VaultLockService'
export * from './VaultLock/VaultLockServiceEvent' export * from './VaultLock/VaultLockServiceEvent'
export * from './VaultLock/VaultLockServiceInterface' export * from './VaultLock/VaultLockServiceInterface'

View File

@@ -4,8 +4,8 @@ import { WebSocketsService } from './../Services/Api/WebsocketsService'
import { MigrationService } from './../Services/Migration/MigrationService' import { MigrationService } from './../Services/Migration/MigrationService'
import { LegacyApiService } from './../Services/Api/ApiService' import { LegacyApiService } from './../Services/Api/ApiService'
import { FeaturesService } from '@Lib/Services/Features/FeaturesService' import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
import { SNPreferencesService } from './../Services/Preferences/PreferencesService' import { PreferencesService } from './../Services/Preferences/PreferencesService'
import { SNProtectionService } from './../Services/Protection/ProtectionService' import { ProtectionService } from './../Services/Protection/ProtectionService'
import { SessionManager } from './../Services/Session/SessionManager' import { SessionManager } from './../Services/Session/SessionManager'
import { HttpService, HttpServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api' import { HttpService, HttpServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
import { ApplicationIdentifier, compareVersions, ProtocolVersion, KeyParamsOrigination } from '@standardnotes/common' import { ApplicationIdentifier, compareVersions, ProtocolVersion, KeyParamsOrigination } from '@standardnotes/common'
@@ -32,7 +32,7 @@ import {
FeaturesClientInterface, FeaturesClientInterface,
ItemManagerInterface, ItemManagerInterface,
SyncServiceInterface, SyncServiceInterface,
UserClientInterface, UserServiceInterface,
MutatorClientInterface, MutatorClientInterface,
StatusServiceInterface, StatusServiceInterface,
AlertService, AlertService,
@@ -74,7 +74,6 @@ import {
VaultUserServiceInterface, VaultUserServiceInterface,
VaultInviteServiceInterface, VaultInviteServiceInterface,
NotificationServiceEvent, NotificationServiceEvent,
VaultServiceEvent,
VaultLockServiceInterface, VaultLockServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
import { import {
@@ -297,7 +296,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
const uninstall = syncService.addEventObserver(syncEventCallback) const uninstall = syncService.addEventObserver(syncEventCallback)
this.serviceObservers.push(uninstall) this.serviceObservers.push(uninstall)
const protectionService = this.dependencies.get<SNProtectionService>(TYPES.ProtectionService) const protectionService = this.dependencies.get<ProtectionService>(TYPES.ProtectionService)
this.serviceObservers.push( this.serviceObservers.push(
protectionService.addEventObserver((event) => { protectionService.addEventObserver((event) => {
if (event === ProtectionEvent.UnprotectedSessionBegan) { if (event === ProtectionEvent.UnprotectedSessionBegan) {
@@ -329,7 +328,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}), }),
) )
const preferencesService = this.dependencies.get<SNPreferencesService>(TYPES.PreferencesService) const preferencesService = this.dependencies.get<PreferencesService>(TYPES.PreferencesService)
this.serviceObservers.push( this.serviceObservers.push(
preferencesService.addEventObserver(() => { preferencesService.addEventObserver(() => {
void this.notifyEvent(ApplicationEvent.PreferencesChanged) void this.notifyEvent(ApplicationEvent.PreferencesChanged)
@@ -1141,7 +1140,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.dependencies.get(TYPES.SharedVaultService), this.dependencies.get(TYPES.SharedVaultService),
NotificationServiceEvent.NotificationReceived, NotificationServiceEvent.NotificationReceived,
) )
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), VaultServiceEvent.VaultRootKeyRotated)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults) this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults)
this.events.addEventHandler( this.events.addEventHandler(
@@ -1267,8 +1265,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<SyncServiceInterface>(TYPES.SyncService) return this.dependencies.get<SyncServiceInterface>(TYPES.SyncService)
} }
public get user(): UserClientInterface { public get user(): UserServiceInterface {
return this.dependencies.get<UserClientInterface>(TYPES.UserService) return this.dependencies.get<UserServiceInterface>(TYPES.UserService)
} }
public get settings(): SettingsService { public get settings(): SettingsService {

File diff suppressed because it is too large Load Diff

View File

@@ -153,6 +153,9 @@ export const TYPES = {
IsVaultOwner: Symbol.for('IsVaultOwner'), IsVaultOwner: Symbol.for('IsVaultOwner'),
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'), RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'), ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
GetKeyPairs: Symbol.for('GetKeyPairs'),
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
// Mappers // Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'), SessionStorageMapper: Symbol.for('SessionStorageMapper'),

View File

@@ -1,4 +1,4 @@
import { SNPreferencesService } from '../Preferences/PreferencesService' import { PreferencesService } from '../Preferences/PreferencesService'
import { GenericItem, Environment, Platform } from '@standardnotes/models' import { GenericItem, Environment, Platform } from '@standardnotes/models'
import { import {
InternalEventBusInterface, InternalEventBusInterface,
@@ -68,7 +68,7 @@ describe('featuresService', () => {
features = {} as jest.Mocked<FeaturesService> features = {} as jest.Mocked<FeaturesService>
prefs = {} as jest.Mocked<SNPreferencesService> prefs = {} as jest.Mocked<PreferencesService>
prefs.addEventObserver = jest.fn() prefs.addEventObserver = jest.fn()
alerts = {} as jest.Mocked<AlertService> alerts = {} as jest.Mocked<AlertService>

View File

@@ -20,7 +20,7 @@ import {
StorageServiceInterface, StorageServiceInterface,
SubscriptionManagerInterface, SubscriptionManagerInterface,
SyncServiceInterface, SyncServiceInterface,
UserClientInterface, UserServiceInterface,
UserService, UserService,
} from '@standardnotes/services' } from '@standardnotes/services'
import { LegacyApiService, SessionManager } from '../Api' import { LegacyApiService, SessionManager } from '../Api'
@@ -37,7 +37,7 @@ describe('FeaturesService', () => {
let apiService: LegacyApiServiceInterface let apiService: LegacyApiServiceInterface
let webSocketsService: WebSocketsService let webSocketsService: WebSocketsService
let settingsService: SettingsClientInterface let settingsService: SettingsClientInterface
let userService: UserClientInterface let userService: UserServiceInterface
let syncService: SyncServiceInterface let syncService: SyncServiceInterface
let alertService: AlertService let alertService: AlertService
let sessionManager: SessionsClientInterface let sessionManager: SessionsClientInterface

View File

@@ -43,7 +43,7 @@ import {
ItemManagerInterface, ItemManagerInterface,
SyncServiceInterface, SyncServiceInterface,
SessionsClientInterface, SessionsClientInterface,
UserClientInterface, UserServiceInterface,
SubscriptionManagerInterface, SubscriptionManagerInterface,
AccountEvent, AccountEvent,
SubscriptionManagerEvent, SubscriptionManagerEvent,
@@ -76,7 +76,7 @@ export class FeaturesService
private api: LegacyApiServiceInterface, private api: LegacyApiServiceInterface,
sockets: WebSocketsService, sockets: WebSocketsService,
private settings: SettingsClientInterface, private settings: SettingsClientInterface,
private user: UserClientInterface, private user: UserServiceInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
private alerts: AlertService, private alerts: AlertService,
private sessions: SessionsClientInterface, private sessions: SessionsClientInterface,

View File

@@ -14,7 +14,7 @@ import {
MutatorClientInterface, MutatorClientInterface,
SyncServiceInterface, SyncServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
import { SNProtectionService } from '../Protection' import { ProtectionService } from '../Protection'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class ListedService extends AbstractService implements ListedClientInterface { export class ListedService extends AbstractService implements ListedClientInterface {
@@ -23,7 +23,7 @@ export class ListedService extends AbstractService implements ListedClientInterf
private itemManager: ItemManager, private itemManager: ItemManager,
private settingsService: SettingsService, private settingsService: SettingsService,
private httpSerivce: DeprecatedHttpService, private httpSerivce: DeprecatedHttpService,
private protectionService: SNProtectionService, private protectionService: ProtectionService,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncServiceInterface, private sync: SyncServiceInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,

View File

@@ -56,6 +56,14 @@ describe('mutator service', () => {
return mutatorService.insertItem(note) return mutatorService.insertItem(note)
} }
describe('insertItem', () => {
it('should throw if attempting to insert already inserted item', async () => {
const note = await insertNote('hello')
expect(mutatorService.insertItem(note)).rejects.toThrow()
})
})
describe('note modifications', () => { describe('note modifications', () => {
it('pinning should not update timestamps', async () => { it('pinning should not update timestamps', async () => {
const note = await insertNote('hello') const note = await insertNote('hello')

View File

@@ -358,6 +358,11 @@ export class MutatorService extends AbstractService implements MutatorClientInte
} }
public async insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty = true): Promise<T> { public async insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty = true): Promise<T> {
const existingItem = this.itemManager.findItem<T>(item.uuid)
if (existingItem) {
throw Error('Attempting to insert item that already exists')
}
if (setDirty) { if (setDirty) {
const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps) const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps)
const dirtiedPayload = mutator.getResult() const dirtiedPayload = mutator.getResult()

View File

@@ -17,7 +17,7 @@ import {
} from '@standardnotes/services' } from '@standardnotes/services'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
export class SNPreferencesService export class PreferencesService
extends AbstractService<PreferencesServiceEvent> extends AbstractService<PreferencesServiceEvent>
implements PreferenceServiceInterface, InternalEventHandlerInterface implements PreferenceServiceInterface, InternalEventHandlerInterface
{ {

View File

@@ -1,6 +1,6 @@
import { ChallengeService } from '../Challenge' import { ChallengeService } from '../Challenge'
import { DiskStorageService } from '../Storage/DiskStorageService' import { DiskStorageService } from '../Storage/DiskStorageService'
import { SNProtectionService } from './ProtectionService' import { ProtectionService } from './ProtectionService'
import { import {
InternalEventBus, InternalEventBus,
InternalEventBusInterface, InternalEventBusInterface,
@@ -28,10 +28,10 @@ describe('protectionService', () => {
let challengeService: ChallengeService let challengeService: ChallengeService
let storageService: DiskStorageService let storageService: DiskStorageService
let internalEventBus: InternalEventBusInterface let internalEventBus: InternalEventBusInterface
let protectionService: SNProtectionService let protectionService: ProtectionService
const createService = () => { const createService = () => {
return new SNProtectionService(encryptionService, mutator, challengeService, storageService, internalEventBus) return new ProtectionService(encryptionService, mutator, challengeService, storageService, internalEventBus)
} }
const createFile = (name: string, isProtected?: boolean) => { const createFile = (name: string, isProtected?: boolean) => {

View File

@@ -74,7 +74,7 @@ export const ProtectionSessionDurations = [
* like viewing a protected note, as well as managing how long that * like viewing a protected note, as well as managing how long that
* authentication should be valid for. * authentication should be valid for.
*/ */
export class SNProtectionService export class ProtectionService
extends AbstractService<ProtectionEvent> extends AbstractService<ProtectionEvent>
implements ProtectionsClientInterface, InternalEventHandlerInterface implements ProtectionsClientInterface, InternalEventHandlerInterface
{ {

View File

@@ -32,8 +32,9 @@ import {
ApplicationEvent, ApplicationEvent,
ApplicationStageChangedEventPayload, ApplicationStageChangedEventPayload,
ApplicationStage, ApplicationStage,
GetKeyPairs,
} from '@standardnotes/services' } from '@standardnotes/services'
import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { import {
SessionBody, SessionBody,
ErrorTag, ErrorTag,
@@ -103,6 +104,7 @@ export class SessionManager
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>, private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>, private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
private workspaceIdentifier: string, private workspaceIdentifier: string,
private _getKeyPairs: GetKeyPairs,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -215,11 +217,15 @@ export class SessionManager
} }
public getPublicKey(): string { public getPublicKey(): string {
return this.encryptionService.getKeyPair().publicKey const keys = this._getKeyPairs.execute()
return keys.getValue().encryption.publicKey
} }
public getSigningPublicKey(): string { public getSigningPublicKey(): string {
return this.encryptionService.getSigningKeyPair().publicKey const keys = this._getKeyPairs.execute()
return keys.getValue().signing.publicKey
} }
public get userUuid(): string { public get userUuid(): string {
@@ -625,15 +631,7 @@ export class SessionManager
newEmail: parameters.newEmail, newEmail: parameters.newEmail,
}) })
let oldKeyPair: PkcKeyPair | undefined const oldKeys = this._getKeyPairs.execute()
let oldSigningKeyPair: PkcKeyPair | undefined
try {
oldKeyPair = this.encryptionService.getKeyPair()
oldSigningKeyPair = this.encryptionService.getSigningKeyPair()
} catch (error) {
void error
}
const processedResponse = await this.processChangeCredentialsResponse( const processedResponse = await this.processChangeCredentialsResponse(
rawResponse, rawResponse,
@@ -644,13 +642,12 @@ export class SessionManager
if (!isErrorResponse(rawResponse)) { if (!isErrorResponse(rawResponse)) {
if (InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { if (InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
const eventData: UserKeyPairChangedEventData = { const eventData: UserKeyPairChangedEventData = {
previous: previous: !oldKeys.isFailed()
oldKeyPair && oldSigningKeyPair ? {
? { encryption: oldKeys.getValue().encryption,
encryption: oldKeyPair, signing: oldKeys.getValue().signing,
signing: oldSigningKeyPair, }
} : undefined,
: undefined,
current: { current: {
encryption: parameters.newRootKey.encryptionKeyPair, encryption: parameters.newRootKey.encryptionKeyPair,
signing: parameters.newRootKey.signingKeyPair, signing: parameters.newRootKey.signingKeyPair,

View File

@@ -9,6 +9,7 @@ export const VaultTests = {
'vaults/importing.test.js', 'vaults/importing.test.js',
'vaults/asymmetric-messages.test.js', 'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js', 'vaults/keypair-change.test.js',
'vaults/key-sharing.test.js',
'vaults/signatures.test.js', 'vaults/signatures.test.js',
'vaults/shared_vaults.test.js', 'vaults/shared_vaults.test.js',
'vaults/invites.test.js', 'vaults/invites.test.js',

View File

@@ -149,16 +149,24 @@ export class AppContext {
return this.application.asymmetric return this.application.asymmetric
} }
get keyPair() {
return this.application.dependencies.get(TYPES.GetKeyPairs).execute().getValue().encryption
}
get signingKeyPair() {
return this.application.dependencies.get(TYPES.GetKeyPairs).execute().getValue().signing
}
get publicKey() { get publicKey() {
return this.sessions.getPublicKey() return this.keyPair.publicKey
} }
get signingPublicKey() { get signingPublicKey() {
return this.sessions.getSigningPublicKey() return this.signingKeyPair.publicKey
} }
get privateKey() { get privateKey() {
return this.encryption.getKeyPair().privateKey return this.keyPair.privateKey
} }
ignoreChallenges() { ignoreChallenges() {
@@ -453,34 +461,6 @@ export class AppContext {
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'shareContactWithVaults') return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'shareContactWithVaults')
} }
resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.systemIdentifier === targetVault.systemIdentifier) {
resolve()
}
return result
})
})
}
resolveWhenSharedVaultChangeInvitesAreSent(sharedVaultUuid) {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => {
objectToSpy.handleVaultRootKeyRotatedEvent.restore()
const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault)
if (vault.sharing.sharedVaultUuid === sharedVaultUuid) {
resolve()
}
return result
})
})
}
awaitUserPrefsSingletonCreation() { awaitUserPrefsSingletonCreation() {
const preferences = this.application.preferences.preferences const preferences = this.application.preferences.preferences
if (preferences) { if (preferences) {

View File

@@ -1,4 +1,5 @@
import { AppContext } from './AppContext.js' import { AppContext } from './AppContext.js'
import * as Collaboration from './Collaboration.js'
export class VaultsContext extends AppContext { export class VaultsContext extends AppContext {
constructor(params) { constructor(params) {
@@ -6,17 +7,22 @@ export class VaultsContext extends AppContext {
} }
async changeVaultName(vault, nameAndDesc) { async changeVaultName(vault, nameAndDesc) {
const sendDataChangePromise = this.resolveWhenAsyncFunctionCompletes(
this.sharedVaults._sendVaultDataChangeMessage,
'execute',
)
await this.vaults.changeVaultNameAndDescription(vault, { await this.vaults.changeVaultNameAndDescription(vault, {
name: nameAndDesc.name, name: nameAndDesc.name,
description: nameAndDesc.description, description: nameAndDesc.description,
}) })
}
await this.awaitPromiseOrThrow(sendDataChangePromise, undefined, 'Waiting for vault data change message to process') getKeyPair() {
const result = this.application.dependencies.get(TYPES.GetKeyPairs).execute()
return result.getValue().encryption
}
getSigningKeyPair() {
const result = this.application.dependencies.get(TYPES.GetKeyPairs).execute()
return result.getValue().signing
} }
async changePassword(password) { async changePassword(password) {
@@ -51,4 +57,28 @@ export class VaultsContext extends AppContext {
async runAnyRequestToPreventRefreshTokenFromExpiring() { async runAnyRequestToPreventRefreshTokenFromExpiring() {
await this.asymmetric.getInboundMessages() await this.asymmetric.getInboundMessages()
} }
async createSharedPasswordVault(password) {
const privateVault = await this.vaults.createUserInputtedPasswordVault({
name: 'Our Vault',
userInputtedPassword: password,
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await this.createSyncedNote('foo', 'bar')
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,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
return { sharedVault, thirdPartyContext, deinitThirdPartyContext }
}
} }

View File

@@ -158,7 +158,8 @@ export async function registerOldUser({ application, email, password, version })
mode: SyncMode.DownloadFirst, mode: SyncMode.DownloadFirst,
...syncOptions, ...syncOptions,
}) })
await application.encryption.decryptErroredPayloads()
await application.dependencies.get(TYPES.DecryptErroredPayloads).execute()
} }
export function createStorageItemPayload(contentType) { export function createStorageItemPayload(contentType) {

View File

@@ -1,3 +1,5 @@
import * as Factory from './lib/factory.js'
describe.skip('session invalidation tests to revisit', function () { describe.skip('session invalidation tests to revisit', function () {
this.timeout(Factory.TwentySecondTimeout) this.timeout(Factory.TwentySecondTimeout)

View File

@@ -189,12 +189,7 @@ describe('asymmetric messages', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const promise = context.resolveWhenAsyncFunctionCompletes(
context.sharedVaults._notifyVaultUsersOfKeyRotation,
'execute',
)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
@@ -333,8 +328,8 @@ describe('asymmetric messages', function () {
it('sender keypair changed message should be signed using old key pair', async () => { it('sender keypair changed message should be signed using old key pair', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const oldKeyPair = context.encryption.getKeyPair() const oldKeyPair = context.keyPair
const oldSigningKeyPair = context.encryption.getSigningKeyPair() const oldSigningKeyPair = context.signingKeyPair
await context.changePassword('new password') await context.changePassword('new password')
@@ -360,8 +355,8 @@ describe('asymmetric messages', function () {
await context.changePassword('new password') await context.changePassword('new password')
const newKeyPair = context.encryption.getKeyPair() const newKeyPair = context.keyPair
const newSigningKeyPair = context.encryption.getSigningKeyPair() const newSigningKeyPair = context.signingKeyPair
await contactContext.syncAndAwaitMessageProcessing() await contactContext.syncAndAwaitMessageProcessing()
@@ -413,12 +408,12 @@ describe('asymmetric messages', function () {
const usecase = context.application.dependencies.get(TYPES.ResendAllMessages) const usecase = context.application.dependencies.get(TYPES.ResendAllMessages)
const result = await usecase.execute({ const result = await usecase.execute({
keys: { keys: {
encryption: context.encryption.getKeyPair(), encryption: context.keyPair,
signing: context.encryption.getSigningKeyPair(), signing: context.signingKeyPair,
}, },
previousKeys: { previousKeys: {
encryption: context.encryption.getKeyPair(), encryption: context.keyPair,
signing: context.encryption.getSigningKeyPair(), signing: context.signingKeyPair,
}, },
}) })

View File

@@ -31,8 +31,8 @@ describe('shared vault crypto', function () {
let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier) let recreatedContext = await Factory.createVaultsContextWithRealCrypto(appIdentifier)
await recreatedContext.launch() await recreatedContext.launch()
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined expect(recreatedContext.keyPair).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined expect(recreatedContext.signingKeyPair).to.not.be.undefined
await recreatedContext.deinit() await recreatedContext.deinit()
}) })

View File

@@ -28,7 +28,11 @@ describe('shared vault invites', function () {
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
const vaultInvite = ( const vaultInvite = (
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultUserPermission.PERMISSIONS.Write) await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
contact,
SharedVaultUserPermission.PERMISSIONS.Write,
)
).getValue() ).getValue()
expect(vaultInvite).to.not.be.undefined expect(vaultInvite).to.not.be.undefined
@@ -100,7 +104,11 @@ describe('shared vault invites', function () {
/** Sync the contact context so that they wouldn't naturally receive changes made before this point */ /** Sync the contact context so that they wouldn't naturally receive changes made before this point */
await contactContext.sync() await contactContext.sync()
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultUserPermission.PERMISSIONS.Write) await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
contact,
SharedVaultUserPermission.PERMISSIONS.Write,
)
/** Contact should now sync and expect to find note */ /** Contact should now sync and expect to find note */
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
@@ -214,31 +222,4 @@ describe('shared vault invites', function () {
await deinitContactContext() await deinitContactContext()
}) })
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
}) })

View File

@@ -110,7 +110,7 @@ describe('vault key management', function () {
await context.vaultLocks.lockNonPersistentVault(vault) await context.vaultLocks.lockNonPersistentVault(vault)
await Factory.expectThrowsAsync( await Factory.expectThrowsAsync(
() => context.vaults.changeVaultOptions({ vault }), () => context.vaults.changeVaultKeyOptions({ vault }),
'Attempting to change vault options on a locked vault', 'Attempting to change vault options on a locked vault',
) )
}) })
@@ -206,7 +206,7 @@ describe('vault key management', function () {
}) })
}) })
describe('changeVaultOptions', () => { describe('changeVaultKeyOptions', () => {
describe('change storage type', () => { describe('change storage type', () => {
it('should not be able to change randomized vault from synced to local', async () => { it('should not be able to change randomized vault from synced to local', async () => {
const vault = await context.vaults.createRandomizedVault({ const vault = await context.vaults.createRandomizedVault({
@@ -214,13 +214,13 @@ describe('vault key management', function () {
description: 'test vault description', description: 'test vault description',
}) })
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Local, newStorageMode: KeySystemRootKeyStorageMode.Local,
}) })
expect(result.isFailed()).to.be.true expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
}) })
it('should not be able to change randomized vault from synced to ephemeral', async () => { it('should not be able to change randomized vault from synced to ephemeral', async () => {
@@ -229,13 +229,13 @@ describe('vault key management', function () {
description: 'test vault description', description: 'test vault description',
}) })
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Local, newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
}) })
expect(result.isFailed()).to.be.true expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
}) })
it('should change user password vault from synced to local', async () => { it('should change user password vault from synced to local', async () => {
@@ -248,7 +248,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Local, newStorageMode: KeySystemRootKeyStorageMode.Local,
}) })
@@ -272,7 +272,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Ephemeral, newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
}) })
@@ -299,7 +299,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Synced, newStorageMode: KeySystemRootKeyStorageMode.Synced,
}) })
@@ -326,7 +326,7 @@ describe('vault key management', function () {
let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) let syncedKeys = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newStorageMode: KeySystemRootKeyStorageMode.Ephemeral, newStorageMode: KeySystemRootKeyStorageMode.Ephemeral,
}) })
@@ -351,9 +351,9 @@ describe('vault key management', function () {
description: 'test vault description', description: 'test vault description',
}) })
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newPasswordType: { newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted, passwordType: KeySystemPasswordType.UserInputted,
}, },
}) })
@@ -370,9 +370,9 @@ describe('vault key management', function () {
const rootKeysBeforeChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) const rootKeysBeforeChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
expect(rootKeysBeforeChange.length).to.equal(1) expect(rootKeysBeforeChange.length).to.equal(1)
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newPasswordType: { newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted, passwordType: KeySystemPasswordType.UserInputted,
userInputtedPassword: 'test password', userInputtedPassword: 'test password',
}, },
@@ -394,9 +394,9 @@ describe('vault key management', function () {
storagePreference: KeySystemRootKeyStorageMode.Local, storagePreference: KeySystemRootKeyStorageMode.Local,
}) })
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newPasswordType: { newPasswordOptions: {
passwordType: KeySystemPasswordType.Randomized, passwordType: KeySystemPasswordType.Randomized,
}, },
}) })
@@ -404,7 +404,7 @@ describe('vault key management', function () {
expect(result.isFailed()).to.be.false expect(result.isFailed()).to.be.false
const rootKeysAfterChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) const rootKeysAfterChange = context.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
expect(rootKeysAfterChange.length).to.equal(1) expect(rootKeysAfterChange.length).to.equal(2)
const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier) const storedKey = context.keys.getRootKeyFromStorageForVault(vault.systemIdentifier)
expect(storedKey).to.be.undefined expect(storedKey).to.be.undefined
@@ -421,9 +421,9 @@ describe('vault key management', function () {
storagePreference: KeySystemRootKeyStorageMode.Local, storagePreference: KeySystemRootKeyStorageMode.Local,
}) })
const result = await context.vaults.changeVaultOptions({ const result = await context.vaults.changeVaultKeyOptions({
vault, vault,
newPasswordType: { newPasswordOptions: {
passwordType: KeySystemPasswordType.Randomized, passwordType: KeySystemPasswordType.Randomized,
}, },
newStorageMode: KeySystemRootKeyStorageMode.Local, newStorageMode: KeySystemRootKeyStorageMode.Local,
@@ -431,7 +431,7 @@ describe('vault key management', function () {
expect(result.isFailed()).to.be.true expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Vault uses randomized password and cannot change its storage preference') expect(result.getError()).to.equal('Cannot change storage mode to non-synced for randomized vault')
}) })
}) })
}) })

View File

@@ -32,9 +32,7 @@ describe('vault key rotation', function () {
const callSpy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption') const callSpy = sinon.spy(context.keys, 'queueVaultItemsKeysForReencryption')
const syncSpy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer') const syncSpy = context.spyOnFunctionResult(context.application.sync, 'payloadsByPreparingForServer')
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
await syncSpy await syncSpy
expect(callSpy.callCount).to.equal(1) expect(callSpy.callCount).to.equal(1)
@@ -95,12 +93,10 @@ describe('vault key rotation', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const outboundMessages = (await context.asymmetric.getOutboundMessages()).getValue() const outboundMessages = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change'] const expectedMessages = ['root key change']
expect(outboundMessages.length).to.equal(expectedMessages.length) expect(outboundMessages.length).to.equal(expectedMessages.length)
const message = outboundMessages[0] const message = outboundMessages[0]
@@ -122,9 +118,7 @@ describe('vault key rotation', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
@@ -150,9 +144,7 @@ describe('vault key rotation', function () {
const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(previousPrimaryItemsKey).to.not.be.undefined expect(previousPrimaryItemsKey).to.not.be.undefined
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
contactContext.unlockSyncing() contactContext.unlockSyncing()
await contactContext.syncAndAwaitMessageProcessing() await contactContext.syncAndAwaitMessageProcessing()
@@ -175,9 +167,7 @@ describe('vault key rotation', function () {
expect(originalOutboundInvites.length).to.equal(1) expect(originalOutboundInvites.length).to.equal(1)
const originalInviteMessage = originalOutboundInvites[0].encrypted_message const originalInviteMessage = originalOutboundInvites[0].encrypted_message
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const updatedOutboundInvites = await context.vaultInvites.getOutboundInvites() const updatedOutboundInvites = await context.vaultInvites.getOutboundInvites()
expect(updatedOutboundInvites.length).to.equal(1) expect(updatedOutboundInvites.length).to.equal(1)
@@ -211,19 +201,15 @@ describe('vault key rotation', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await firstPromise
const asymmetricMessageAfterFirstChange = (await context.asymmetric.getOutboundMessages()).getValue() const asymmetricMessageAfterFirstChange = (await context.asymmetric.getOutboundMessages()).getValue()
const expectedMessages = ['root key change', 'vault metadata change'] const expectedMessages = ['root key change']
expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length) expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length)
const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0] const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0]
const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await secondPromise
const asymmetricMessageAfterSecondChange = (await context.asymmetric.getOutboundMessages()).getValue() const asymmetricMessageAfterSecondChange = (await context.asymmetric.getOutboundMessages()).getValue()
expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length) expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length)
@@ -245,9 +231,7 @@ describe('vault key rotation', function () {
) )
contactContext.lockSyncing() contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault) await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')

View File

@@ -0,0 +1,79 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('vault key sharing', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createVaultsContextWithRealCrypto()
await context.launch()
await context.register()
})
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const rootKey = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(rootKey).to.not.be.undefined
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
it('should send key change message when vault password is changed', async () => {
const { sharedVault, thirdPartyContext, deinitThirdPartyContext } = await context.createSharedPasswordVault(
'test password',
)
const rootKeyBeforeChange = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
await context.vaults.changeVaultKeyOptions({
vault: sharedVault,
newPasswordOptions: {
passwordType: KeySystemPasswordType.UserInputted,
userInputtedPassword: 'new password',
},
})
await thirdPartyContext.syncAndAwaitMessageProcessing()
const rootKeyAfterChange = thirdPartyContext.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(rootKeyBeforeChange.itemsKey).to.not.equal(rootKeyAfterChange.itemsKey)
await deinitThirdPartyContext()
})
})

View File

@@ -82,13 +82,14 @@ describe('keypair change', function () {
contactContext.lockSyncing() contactContext.lockSyncing()
const previousKeyPair = context.encryption.getKeyPair() const previousKeyPair = context.keyPair
const previousSigningKeyPair = context.encryption.getSigningKeyPair() const previousSigningKeyPair = context.signingKeyPair
await context.changePassword('new_password') await context.changePassword('new_password')
sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair) sinon
sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair) .stub(context.application.dependencies.get(TYPES.GetKeyPairs), 'execute')
.returns(Result.ok({ encryption: previousKeyPair, signing: previousSigningKeyPair }))
await context.changeVaultName(sharedVault, { await context.changeVaultName(sharedVault, {
name: 'New Name', name: 'New Name',

View File

@@ -28,11 +28,11 @@ describe('public key cryptography', function () {
}) })
it('should create keypair during registration', () => { it('should create keypair during registration', () => {
expect(sessions.getPublicKey()).to.not.be.undefined expect(context.publicKey).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined expect(context.keyPair.privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.be.undefined expect(context.signingPublicKey).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined expect(context.signingKeyPair.privateKey).to.not.be.undefined
}) })
it('should populate keypair during sign in', async () => { it('should populate keypair during sign in', async () => {
@@ -46,33 +46,33 @@ describe('public key cryptography', function () {
recreatedContext.password = password recreatedContext.password = password
await recreatedContext.signIn() await recreatedContext.signIn()
expect(recreatedContext.sessions.getPublicKey()).to.not.be.undefined expect(recreatedContext.publicKey).to.not.be.undefined
expect(recreatedContext.encryption.getKeyPair().privateKey).to.not.be.undefined expect(recreatedContext.privateKey).to.not.be.undefined
expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined expect(recreatedContext.signingPublicKey).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined expect(recreatedContext.signingKeyPair.privateKey).to.not.be.undefined
await recreatedContext.deinit() await recreatedContext.deinit()
}) })
it('should rotate keypair during password change', async () => { it('should rotate keypair during password change', async () => {
const oldPublicKey = sessions.getPublicKey() const oldPublicKey = context.publicKey
const oldPrivateKey = encryption.getKeyPair().privateKey const oldPrivateKey = context.privateKey
const oldSigningPublicKey = sessions.getSigningPublicKey() const oldSigningPublicKey = context.signingPublicKey
const oldSigningPrivateKey = encryption.getSigningKeyPair().privateKey const oldSigningPrivateKey = context.signingKeyPair.privateKey
await context.changePassword('new_password') await context.changePassword('new_password')
expect(sessions.getPublicKey()).to.not.be.undefined expect(context.publicKey).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined expect(context.keyPair.privateKey).to.not.be.undefined
expect(sessions.getPublicKey()).to.not.equal(oldPublicKey) expect(context.publicKey).to.not.equal(oldPublicKey)
expect(encryption.getKeyPair().privateKey).to.not.equal(oldPrivateKey) expect(context.keyPair.privateKey).to.not.equal(oldPrivateKey)
expect(sessions.getSigningPublicKey()).to.not.be.undefined expect(context.signingPublicKey).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined expect(context.signingKeyPair.privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.equal(oldSigningPublicKey) expect(context.signingPublicKey).to.not.equal(oldSigningPublicKey)
expect(encryption.getSigningKeyPair().privateKey).to.not.equal(oldSigningPrivateKey) expect(context.signingKeyPair.privateKey).to.not.equal(oldSigningPrivateKey)
}) })
it('should allow option to enable collaboration for previously signed in accounts', async () => { it('should allow option to enable collaboration for previously signed in accounts', async () => {

View File

@@ -87,7 +87,7 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
const isChangingPasswordType = vault.keyPasswordType !== passwordType const isChangingPasswordType = vault.keyPasswordType !== passwordType
const isChangingKeyStorageMode = vault.keyStorageMode !== keyStorageMode const isChangingKeyStorageMode = vault.keyStorageMode !== keyStorageMode
const getPasswordTypeParams = (): ChangeVaultKeyOptionsDTO['newPasswordType'] => { const getPasswordTypeParams = (): ChangeVaultKeyOptionsDTO['newPasswordOptions'] => {
if (!isChangingPasswordType) { if (!isChangingPasswordType) {
throw new Error('Password type is not changing') throw new Error('Password type is not changing')
} }
@@ -108,9 +108,9 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
} }
if (isChangingPasswordType || isChangingKeyStorageMode) { if (isChangingPasswordType || isChangingKeyStorageMode) {
await application.vaults.changeVaultOptions({ await application.vaults.changeVaultKeyOptions({
vault, vault,
newPasswordType: isChangingPasswordType ? getPasswordTypeParams() : undefined, newPasswordOptions: isChangingPasswordType ? getPasswordTypeParams() : undefined,
newStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined, newStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined,
}) })
} }

View File

@@ -17,7 +17,7 @@ import {
SyncOpStatus, SyncOpStatus,
SyncServiceInterface, SyncServiceInterface,
User, User,
UserClientInterface, UserServiceInterface,
UserRequestType, UserRequestType,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
@@ -41,7 +41,7 @@ describe('ApplicationEventObserver', () => {
let sessionManager: SessionsClientInterface let sessionManager: SessionsClientInterface
let subscriptionManager: SubscriptionManagerInterface let subscriptionManager: SubscriptionManagerInterface
let toastService: ToastServiceInterface let toastService: ToastServiceInterface
let userService: UserClientInterface let userService: UserServiceInterface
const createObserver = () => const createObserver = () =>
new ApplicationEventObserver( new ApplicationEventObserver(
@@ -94,7 +94,7 @@ describe('ApplicationEventObserver', () => {
toastService.showToast = jest.fn().mockReturnValue('1') toastService.showToast = jest.fn().mockReturnValue('1')
toastService.hideToast = jest.fn() toastService.hideToast = jest.fn()
userService = {} as jest.Mocked<UserClientInterface> userService = {} as jest.Mocked<UserServiceInterface>
userService.submitUserRequest = jest.fn().mockReturnValue(true) userService.submitUserRequest = jest.fn().mockReturnValue(true)
}) })

View File

@@ -10,7 +10,7 @@ import {
SessionsClientInterface, SessionsClientInterface,
SubscriptionManagerInterface, SubscriptionManagerInterface,
SyncServiceInterface, SyncServiceInterface,
UserClientInterface, UserServiceInterface,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { ToastType } from '@standardnotes/toast' import { ToastType } from '@standardnotes/toast'
@@ -37,7 +37,7 @@ export class ApplicationEventObserver implements EventObserverInterface {
private sessionManager: SessionsClientInterface, private sessionManager: SessionsClientInterface,
private subscriptionManager: SubscriptionManagerInterface, private subscriptionManager: SubscriptionManagerInterface,
private toastService: ToastServiceInterface, private toastService: ToastServiceInterface,
private userService: UserClientInterface, private userService: UserServiceInterface,
) {} ) {}
async handle(event: ApplicationEvent): Promise<void> { async handle(event: ApplicationEvent): Promise<void> {