refactor: application dependency management (#2363)

This commit is contained in:
Mo
2023-07-23 15:54:31 -05:00
committed by GitHub
parent e698b1c990
commit a77535456c
299 changed files with 7415 additions and 4890 deletions

View File

@@ -1,31 +1,121 @@
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { GetVault } from './../Vaults/UseCase/GetVault'
import { GetTrustedPayload } from './UseCase/GetTrustedPayload'
import { ReplaceContactData } from './../Contacts/UseCase/ReplaceContactData'
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
import { FindContact } from './../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from './../Contacts/UseCase/CreateOrEditContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { HttpServiceInterface } from '@standardnotes/api'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { AsymmetricMessageService } from './AsymmetricMessageService'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayloadType } from '@standardnotes/models'
import {
AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged,
AsymmetricMessageSharedVaultInvite,
AsymmetricMessageSharedVaultMetadataChanged,
AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessageTrustedContactShare,
KeySystemRootKeyContentSpecialized,
TrustedContactInterface,
} from '@standardnotes/models'
describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface>
let mutator: jest.Mocked<MutatorClientInterface>
let encryption: jest.Mocked<EncryptionProviderInterface>
let service: AsymmetricMessageService
beforeEach(() => {
const http = {} as jest.Mocked<HttpServiceInterface>
http.delete = jest.fn()
const messageServer = {} as jest.Mocked<AsymmetricMessageServer>
messageServer.deleteMessage = jest.fn()
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
const contacts = {} as jest.Mocked<ContactServiceInterface>
const items = {} as jest.Mocked<ItemManagerInterface>
const sync = {} as jest.Mocked<SyncServiceInterface>
const mutator = {} as jest.Mocked<MutatorClientInterface>
encryption = {} as jest.Mocked<EncryptionProviderInterface>
const createOrEditContact = {} as jest.Mocked<CreateOrEditContact>
const findContact = {} as jest.Mocked<FindContact>
const getAllContacts = {} as jest.Mocked<GetAllContacts>
const replaceContactData = {} as jest.Mocked<ReplaceContactData>
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
const getVault = {} as jest.Mocked<GetVault>
const handleRootKeyChangedMessage = {} as jest.Mocked<HandleRootKeyChangedMessage>
const sendOwnContactChangedMessage = {} as jest.Mocked<SendOwnContactChangeMessage>
const getOutboundMessagesUseCase = {} as jest.Mocked<GetOutboundMessages>
const getInboundMessagesUseCase = {} as jest.Mocked<GetInboundMessages>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
sync = {} as jest.Mocked<SyncServiceInterface>
sync.sync = jest.fn()
mutator = {} as jest.Mocked<MutatorClientInterface>
mutator.changeItem = jest.fn()
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(http, encryption, contacts, items, mutator, sync, eventBus)
service = new AsymmetricMessageService(
messageServer,
encryption,
mutator,
createOrEditContact,
findContact,
getAllContacts,
replaceContactData,
getTrustedPayload,
getVault,
handleRootKeyChangedMessage,
sendOwnContactChangedMessage,
getOutboundMessagesUseCase,
getInboundMessagesUseCase,
getUntrustedPayload,
eventBus,
)
})
describe('sortServerMessages', () => {
it('should prioritize keypair changed messages over other messages', () => {
const messages: AsymmetricMessageServerHash[] = [
{
uuid: 'keypair-changed-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
},
{
uuid: 'misc-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 1,
updated_at_timestamp: 1,
},
]
service.getUntrustedMessagePayload = jest.fn()
service.getServerMessageType = jest.fn().mockImplementation((message) => {
if (message.uuid === 'keypair-changed-message') {
return AsymmetricMessagePayloadType.SenderKeypairChanged
} else {
return AsymmetricMessagePayloadType.ContactShare
}
})
const sorted = service.sortServerMessages(messages)
expect(sorted[0].uuid).toEqual('keypair-changed-message')
expect(sorted[1].uuid).toEqual('misc-message')
const reverseSorted = service.sortServerMessages(messages.reverse())
expect(reverseSorted[0].uuid).toEqual('keypair-changed-message')
expect(reverseSorted[1].uuid).toEqual('misc-message')
})
})
it('should process incoming messages oldest first', async () => {
@@ -50,7 +140,9 @@ describe('AsymmetricMessageService', () => {
const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } }
service.getTrustedMessagePayload = jest.fn().mockReturnValue(trustedPayloadMock)
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(trustedPayloadMock)
const handleTrustedContactShareMessageMock = jest.fn()
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
@@ -60,4 +152,174 @@ describe('AsymmetricMessageService', () => {
expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1])
expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0])
})
it('should handle ContactShare message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedContactShareMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SenderKeypairChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: {
recipientUuid: '1',
newEncryptionPublicKey: 'new-encryption-public-key',
newSigningPublicKey: 'new-signing-public-key',
},
}
service.handleTrustedSenderKeypairChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSenderKeypairChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultRootKeyChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultRootKeyChanged = {
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
data: {
recipientUuid: '1',
rootKey: {} as KeySystemRootKeyContentSpecialized,
},
}
service.handleTrustedSharedVaultRootKeyChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedSharedVaultRootKeyChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should handle SharedVaultMetadataChanged message', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultMetadataChanged = {
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
data: {
recipientUuid: '1',
sharedVaultUuid: 'shared-vault-uuid',
name: 'Vault name',
description: 'Vault description',
},
}
service.handleTrustedVaultMetadataChangedMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.handleTrustedVaultMetadataChangedMessage).toHaveBeenCalledWith(message, decryptedMessagePayload)
})
it('should throw if message type is SharedVaultInvite', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageSharedVaultInvite = {
type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: {
recipientUuid: '1',
},
} as AsymmetricMessageSharedVaultInvite
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await expect(service.handleRemoteReceivedAsymmetricMessages([message])).rejects.toThrow(
'Shared vault invites payloads are not handled as part of asymmetric messages',
)
})
it('should delete message from server after processing', async () => {
const message: AsymmetricMessageServerHash = {
uuid: 'message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
}
const decryptedMessagePayload: AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare,
data: {
recipientUuid: '1',
trustedContact: {} as TrustedContactInterface,
},
}
service.deleteMessageAfterProcessing = jest.fn()
service.handleTrustedContactShareMessage = jest.fn()
service.getTrustedMessagePayload = service.getUntrustedMessagePayload = jest
.fn()
.mockReturnValue(decryptedMessagePayload)
await service.handleRemoteReceivedAsymmetricMessages([message])
expect(service.deleteMessageAfterProcessing).toHaveBeenCalled()
})
})

View File

@@ -1,13 +1,11 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { AbstractService } from '../Service/AbstractService'
import { GetAsymmetricMessageTrustedPayload } from './UseCase/GetAsymmetricMessageTrustedPayload'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { GetTrustedPayload } from './UseCase/GetTrustedPayload'
import {
AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessagePayloadType,
@@ -16,61 +14,68 @@ import {
AsymmetricMessagePayload,
AsymmetricMessageSharedVaultMetadataChanged,
VaultListingMutator,
MutationType,
PayloadEmitSource,
VaultListingInterface,
} from '@standardnotes/models'
import { HandleTrustedSharedVaultRootKeyChangedMessage } from './UseCase/HandleTrustedSharedVaultRootKeyChangedMessage'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { SessionEvent } from '../Session/SessionEvent'
import { AsymmetricMessageServer, HttpServiceInterface } from '@standardnotes/api'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData'
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages'
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages'
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetVault } from '../Vaults/UseCase/GetVault'
import { AsymmetricMessageServiceInterface } from './AsymmetricMessageServiceInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { FindContact } from '../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact'
import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData'
import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export class AsymmetricMessageService
extends AbstractService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{
private messageServer: AsymmetricMessageServer
constructor(
http: HttpServiceInterface,
private messageServer: AsymmetricMessageServer,
private encryption: EncryptionProviderInterface,
private contacts: ContactServiceInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _replaceContactData: ReplaceContactData,
private _getTrustedPayload: GetTrustedPayload,
private _getVault: GetVault,
private _handleRootKeyChangedMessage: HandleRootKeyChangedMessage,
private _sendOwnContactChangedMessage: SendOwnContactChangeMessage,
private _getOutboundMessagesUseCase: GetOutboundMessages,
private _getInboundMessagesUseCase: GetInboundMessages,
private _getUntrustedPayload: GetUntrustedPayload,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
this.messageServer = new AsymmetricMessageServer(http)
eventBus.addEventHandler(this, SyncEvent.ReceivedAsymmetricMessages)
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SessionEvent.UserKeyPairChanged) {
void this.messageServer.deleteAllInboundMessages()
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
}
if (event.type === SyncEvent.ReceivedAsymmetricMessages) {
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
switch (event.type) {
case SessionEvent.UserKeyPairChanged:
void this.messageServer.deleteAllInboundMessages()
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
break
case SyncEvent.ReceivedAsymmetricMessages:
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
break
}
}
public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetOutboundAsymmetricMessages(this.messageServer)
return usecase.execute()
return this._getOutboundMessagesUseCase.execute()
}
public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetInboundAsymmetricMessages(this.messageServer)
return usecase.execute()
return this._getInboundMessagesUseCase.execute()
}
public async downloadAndProcessInboundMessages(): Promise<void> {
@@ -83,118 +88,223 @@ export class AsymmetricMessageService
}
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
if (!data.previous) {
return
}
const useCase = new SendOwnContactChangeMessage(this.encryption, this.messageServer)
const contacts = this._getAllContacts.execute()
if (contacts.isFailed()) {
return
}
const contacts = this.contacts.getAllContacts()
for (const contact of contacts) {
for (const contact of contacts.getValue()) {
if (contact.isMe) {
continue
}
await useCase.execute({
senderOldKeyPair: data.oldKeyPair,
senderOldSigningKeyPair: data.oldSigningKeyPair,
senderNewKeyPair: data.newKeyPair,
senderNewSigningKeyPair: data.newSigningKeyPair,
await this._sendOwnContactChangedMessage.execute({
senderOldKeyPair: data.previous.encryption,
senderOldSigningKeyPair: data.previous.signing,
senderNewKeyPair: data.current.encryption,
senderNewSigningKeyPair: data.current.signing,
contact,
})
}
}
sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] {
const SortedPriorityTypes = [AsymmetricMessagePayloadType.SenderKeypairChanged]
const priority: AsymmetricMessageServerHash[] = []
const regular: AsymmetricMessageServerHash[] = []
const allMessagesOldestFirst = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
const messageTypeMap: Record<string, AsymmetricMessagePayloadType> = {}
for (const message of allMessagesOldestFirst) {
const messageType = this.getServerMessageType(message)
if (!messageType) {
continue
}
messageTypeMap[message.uuid] = messageType
if (SortedPriorityTypes.includes(messageType)) {
priority.push(message)
} else {
regular.push(message)
}
}
const sortedPriority = priority.sort((a, b) => {
const typeA = messageTypeMap[a.uuid]
const typeB = messageTypeMap[b.uuid]
if (typeA !== typeB) {
return SortedPriorityTypes.indexOf(typeA) - SortedPriorityTypes.indexOf(typeB)
}
return a.created_at_timestamp - b.created_at_timestamp
})
const regularMessagesOldestFirst = regular.sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
return [...sortedPriority, ...regularMessagesOldestFirst]
}
getServerMessageType(message: AsymmetricMessageServerHash): AsymmetricMessagePayloadType | undefined {
const result = this.getUntrustedMessagePayload(message)
if (!result) {
return undefined
}
return result.type
}
async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> {
if (messages.length === 0) {
return
}
const sortedMessages = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
const sortedMessages = this.sortServerMessages(messages)
for (const message of sortedMessages) {
const trustedMessagePayload = this.getTrustedMessagePayload(message)
if (!trustedMessagePayload) {
const trustedPayload = this.getTrustedMessagePayload(message)
if (!trustedPayload) {
continue
}
if (trustedMessagePayload.data.recipientUuid !== message.user_uuid) {
continue
}
if (trustedMessagePayload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
await this.handleTrustedSenderKeypairChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) {
await this.handleTrustedSharedVaultRootKeyChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) {
await this.handleVaultMetadataChangedMessage(message, trustedMessagePayload)
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultInvite) {
throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages')
}
await this.deleteMessageAfterProcessing(message)
await this.handleTrustedMessageResult(message, trustedPayload)
}
}
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
const useCase = new GetAsymmetricMessageTrustedPayload(this.encryption, this.contacts)
return useCase.execute({
privateKey: this.encryption.getKeyPair().privateKey,
message,
})
}
private async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {
await this.messageServer.deleteMessage({ messageUuid: message.uuid })
}
async handleVaultMetadataChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultMetadataChanged,
private async handleTrustedMessageResult(
message: AsymmetricMessageServerHash,
payload: AsymmetricMessagePayload,
): Promise<void> {
const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: trustedPayload.data.sharedVaultUuid })
if (!vault) {
if (payload.data.recipientUuid !== message.user_uuid) {
return
}
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.name = trustedPayload.data.name
mutator.description = trustedPayload.data.description
if (payload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
await this.handleTrustedSenderKeypairChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) {
await this.handleTrustedSharedVaultRootKeyChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) {
await this.handleTrustedVaultMetadataChangedMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SharedVaultInvite) {
throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages')
}
await this.deleteMessageAfterProcessing(message)
}
getUntrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
const result = this._getUntrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
message,
})
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
const contact = this._findContact.execute({ userUuid: message.sender_uuid })
if (contact.isFailed()) {
return undefined
}
const result = this._getTrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
sender: contact.getValue(),
message,
})
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {
await this.messageServer.deleteMessage({ messageUuid: message.uuid })
}
async handleTrustedVaultMetadataChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultMetadataChanged,
): Promise<void> {
const vault = this._getVault.execute<VaultListingInterface>({
sharedVaultUuid: trustedPayload.data.sharedVaultUuid,
})
if (vault.isFailed()) {
return
}
await this.mutator.changeItem<VaultListingMutator>(
vault.getValue(),
(mutator) => {
mutator.name = trustedPayload.data.name
mutator.description = trustedPayload.data.description
},
MutationType.UpdateUserTimestamps,
PayloadEmitSource.RemoteRetrieved,
)
}
async handleTrustedContactShareMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageTrustedContactShare,
): Promise<void> {
await this.contacts.createOrUpdateTrustedContactFromContactShare(trustedPayload.data.trustedContact)
if (trustedPayload.data.trustedContact.isMe) {
return
}
await this._replaceContactData.execute(trustedPayload.data.trustedContact)
}
private async handleTrustedSenderKeypairChangedMessage(
async handleTrustedSenderKeypairChangedMessage(
message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSenderKeypairChanged,
): Promise<void> {
await this.contacts.createOrEditTrustedContact({
await this._createOrEditContact.execute({
contactUuid: message.sender_uuid,
publicKey: trustedPayload.data.newEncryptionPublicKey,
signingPublicKey: trustedPayload.data.newSigningPublicKey,
})
}
private async handleTrustedSharedVaultRootKeyChangedMessage(
async handleTrustedSharedVaultRootKeyChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged,
): Promise<void> {
const useCase = new HandleTrustedSharedVaultRootKeyChangedMessage(
this.mutator,
this.items,
this.sync,
this.encryption,
)
await useCase.execute(trustedPayload)
await this._handleRootKeyChangedMessage.execute(trustedPayload)
}
public override deinit(): void {
super.deinit()
;(this.messageServer as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.mutator as unknown) = undefined
;(this._createOrEditContact as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
;(this._replaceContactData as unknown) = undefined
;(this._getTrustedPayload as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._handleRootKeyChangedMessage as unknown) = undefined
;(this._sendOwnContactChangedMessage as unknown) = undefined
;(this._getOutboundMessagesUseCase as unknown) = undefined
;(this._getInboundMessagesUseCase as unknown) = undefined
;(this._getUntrustedPayload as unknown) = undefined
}
}

View File

@@ -1,23 +0,0 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
export class GetAsymmetricMessageTrustedPayload<M extends AsymmetricMessagePayload> {
constructor(private encryption: EncryptionProviderInterface, private contacts: ContactServiceInterface) {}
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
const trustedContact = this.contacts.findTrustedContact(dto.message.sender_uuid)
if (!trustedContact) {
return undefined
}
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
encryptedString: dto.message.encrypted_message,
trustedSender: trustedContact,
privateKey: dto.privateKey,
})
return decryptionResult
}
}

View File

@@ -1,17 +0,0 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
export class GetAsymmetricMessageUntrustedPayload<M extends AsymmetricMessagePayload> {
constructor(private encryption: EncryptionProviderInterface) {}
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
encryptedString: dto.message.encrypted_message,
trustedSender: undefined,
privateKey: dto.privateKey,
})
return decryptionResult
}
}

View File

@@ -1,7 +1,7 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetInboundAsymmetricMessages {
export class GetInboundMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {

View File

@@ -1,7 +1,7 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetOutboundAsymmetricMessages {
export class GetOutboundMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {

View File

@@ -0,0 +1,17 @@
import { AsymmetricMessagePayloadType } from '@standardnotes/models'
const TypesUsingReplaceableIdentifiers = [
AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
]
export function GetReplaceabilityIdentifier(
type: AsymmetricMessagePayloadType,
sharedVaultUuid: string,
keySystemIdentifier: string,
): string | undefined {
if (!TypesUsingReplaceableIdentifiers.includes(type)) {
return undefined
}
return [type, sharedVaultUuid, keySystemIdentifier].join(':')
}

View File

@@ -0,0 +1,22 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessagePayload> {
constructor(private decryptMessage: DecryptMessage) {}
execute<M extends AsymmetricMessagePayload>(dto: {
privateKey: string
message: AsymmetricMessageServerHash
sender: TrustedContactInterface
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
sender: dto.sender,
privateKey: dto.privateKey,
})
return result
}
}

View File

@@ -0,0 +1,21 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
export class GetUntrustedPayload implements SyncUseCaseInterface<AsymmetricMessagePayload> {
constructor(private decryptMessage: DecryptMessage) {}
execute<M extends AsymmetricMessagePayload>(dto: {
privateKey: string
message: AsymmetricMessageServerHash
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
sender: undefined,
privateKey: dto.privateKey,
})
return result
}
}

View File

@@ -1,5 +1,4 @@
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import {
KeySystemRootKeyInterface,
@@ -7,18 +6,19 @@ import {
FillItemContent,
KeySystemRootKeyContent,
VaultListingMutator,
VaultListingInterface,
} from '@standardnotes/models'
import { ContentType } from '@standardnotes/domain-core'
import { GetVaultUseCase } from '../../Vaults/UseCase/GetVault'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { GetVault } from '../../Vaults/UseCase/GetVault'
import { EncryptionProviderInterface } from '../../Encryption/EncryptionProviderInterface'
export class HandleTrustedSharedVaultRootKeyChangedMessage {
export class HandleRootKeyChangedMessage {
constructor(
private mutator: MutatorClientInterface,
private items: ItemManagerInterface,
private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface,
private getVault: GetVault,
) {}
async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> {
@@ -30,9 +30,9 @@ export class HandleTrustedSharedVaultRootKeyChangedMessage {
true,
)
const vault = new GetVaultUseCase(this.items).execute({ keySystemIdentifier: rootKeyContent.systemIdentifier })
if (vault) {
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
const vault = this.getVault.execute<VaultListingInterface>({ keySystemIdentifier: rootKeyContent.systemIdentifier })
if (!vault.isFailed()) {
await this.mutator.changeItem<VaultListingMutator>(vault.getValue(), (mutator) => {
mutator.rootKeyParams = rootKeyContent.keyParams
})
}

View File

@@ -1,7 +1,7 @@
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { HandleTrustedSharedVaultInviteMessage } from './HandleTrustedSharedVaultInviteMessage'
import { CreateOrEditContact } from './../../Contacts/UseCase/CreateOrEditContact'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ProcessAcceptedVaultInvite } from './ProcessAcceptedVaultInvite'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
import { ContentType } from '@standardnotes/domain-core'
import {
AsymmetricMessagePayloadType,
@@ -9,32 +9,25 @@ import {
KeySystemRootKeyContent,
} from '@standardnotes/models'
describe('HandleTrustedSharedVaultInviteMessage', () => {
let mutatorMock: jest.Mocked<MutatorClientInterface>
let syncServiceMock: jest.Mocked<SyncServiceInterface>
let contactServiceMock: jest.Mocked<ContactServiceInterface>
describe('ProcessAcceptedVaultInvite', () => {
let mutator: jest.Mocked<MutatorClientInterface>
let sync: jest.Mocked<SyncServiceInterface>
let createOrEditContact: jest.Mocked<CreateOrEditContact>
beforeEach(() => {
mutatorMock = {
createItem: jest.fn(),
} as any
mutator = {} as jest.Mocked<MutatorClientInterface>
mutator.createItem = jest.fn()
syncServiceMock = {
sync: jest.fn(),
} as any
sync = {} as jest.Mocked<SyncServiceInterface>
sync.sync = jest.fn()
contactServiceMock = {
createOrEditTrustedContact: jest.fn(),
} as any
createOrEditContact = {} as jest.Mocked<CreateOrEditContact>
createOrEditContact.execute = jest.fn()
})
it('should create root key before creating vault listing so that propagated vault listings do not appear as locked', async () => {
const handleTrustedSharedVaultInviteMessage = new HandleTrustedSharedVaultInviteMessage(
mutatorMock,
syncServiceMock,
contactServiceMock,
)
const handleTrustedSharedVaultInviteMessage = new ProcessAcceptedVaultInvite(mutator, sync, createOrEditContact)
createOrEditContact
const testMessage = {
type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: {
@@ -54,11 +47,11 @@ describe('HandleTrustedSharedVaultInviteMessage', () => {
await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid)
const keySystemRootKeyCallIndex = mutatorMock.createItem.mock.calls.findIndex(
const keySystemRootKeyCallIndex = mutator.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.TYPES.KeySystemRootKey,
)
const vaultListingCallIndex = mutatorMock.createItem.mock.calls.findIndex(
const vaultListingCallIndex = mutator.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.TYPES.VaultListing,
)

View File

@@ -1,4 +1,3 @@
import { ContactServiceInterface } from './../../Contacts/ContactServiceInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import {
KeySystemRootKeyInterface,
@@ -11,12 +10,13 @@ import {
} from '@standardnotes/models'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
import { ContentType } from '@standardnotes/domain-core'
import { CreateOrEditContact } from '../../Contacts/UseCase/CreateOrEditContact'
export class HandleTrustedSharedVaultInviteMessage {
export class ProcessAcceptedVaultInvite {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private contacts: ContactServiceInterface,
private createOrEditContact: CreateOrEditContact,
) {}
async execute(
@@ -47,11 +47,7 @@ export class HandleTrustedSharedVaultInviteMessage {
await this.mutator.createItem(ContentType.TYPES.VaultListing, FillItemContentSpecialized(content), true)
for (const contact of trustedContacts) {
if (contact.isMe) {
throw new Error('Should not receive isMe contact from invite')
}
await this.contacts.createOrEditTrustedContact({
await this.createOrEditContact.execute({
name: contact.name,
contactUuid: contact.contactUuid,
publicKey: contact.publicKeySet.encryption,

View File

@@ -0,0 +1,58 @@
import { AsymmetricMessageServerHash, isErrorResponse } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { ResendMessage } from './ResendMessage'
import { FindContact } from '../../Contacts/UseCase/FindContact'
export class ResendAllMessages implements UseCaseInterface<void> {
constructor(
private resendMessage: ResendMessage,
private messageServer: AsymmetricMessageServerInterface,
private findContact: FindContact,
) {}
async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
}): Promise<Result<AsymmetricMessageServerHash[]>> {
const messages = await this.messageServer.getOutboundUserMessages()
if (isErrorResponse(messages)) {
return Result.fail('Failed to get outbound user messages')
}
const errors: string[] = []
for (const message of messages.data.messages) {
const recipient = this.findContact.execute({ userUuid: message.user_uuid })
if (recipient) {
errors.push(`Contact not found for invite ${message.user_uuid}`)
continue
}
await this.resendMessage.execute({
keys: params.keys,
previousKeys: params.previousKeys,
message: message,
recipient,
})
await this.messageServer.deleteMessage({
messageUuid: message.uuid,
})
}
if (errors.length > 0) {
return Result.fail(errors.join(', '))
}
return Result.ok()
}
}

View File

@@ -0,0 +1,58 @@
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { SendMessage } from './SendMessage'
export class ResendMessage implements UseCaseInterface<void> {
constructor(
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessagePayload>,
private sendMessage: SendMessage,
private encryptMessage: EncryptMessage,
) {}
async execute(params: {
keys: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
previousKeys?: {
encryption: PkcKeyPair
signing: PkcKeyPair
}
recipient: TrustedContactInterface
message: AsymmetricMessageServerHash
}): Promise<Result<AsymmetricMessageServerHash>> {
const decryptionResult = this.decryptOwnMessage.execute({
message: params.message.encrypted_message,
privateKey: params.previousKeys?.encryption.privateKey ?? params.keys.encryption.privateKey,
recipientPublicKey: params.recipient.publicKeySet.encryption,
})
if (decryptionResult.isFailed()) {
return Result.fail(decryptionResult.getError())
}
const decryptedMessage = decryptionResult.getValue()
const encryptedMessage = this.encryptMessage.execute({
message: decryptedMessage,
keys: params.keys,
recipientPublicKey: params.recipient.publicKeySet.encryption,
})
if (encryptedMessage.isFailed()) {
return Result.fail(encryptedMessage.getError())
}
const sendMessageResult = await this.sendMessage.execute({
recipientUuid: params.recipient.contactUuid,
encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: params.message.replaceabilityIdentifier,
})
return sendMessageResult
}
}

View File

@@ -1,14 +1,15 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { isErrorResponse, AsymmetricMessageServerHash, getErrorFromErrorResponse } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class SendAsymmetricMessageUseCase {
export class SendMessage implements UseCaseInterface<AsymmetricMessageServerHash> {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(params: {
recipientUuid: string
encryptedMessage: string
replaceabilityIdentifier: string | undefined
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
}): Promise<Result<AsymmetricMessageServerHash>> {
const response = await this.messageServer.createMessage({
recipientUuid: params.recipientUuid,
encryptedMessage: params.encryptedMessage,
@@ -16,9 +17,9 @@ export class SendAsymmetricMessageUseCase {
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response)
return Result.fail(getErrorFromErrorResponse(response).message)
}
return response.data.message
return Result.ok(response.data.message)
}
}

View File

@@ -1,16 +1,16 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import {
TrustedContactInterface,
AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged,
} from '@standardnotes/models'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendAsymmetricMessageUseCase } from './SendAsymmetricMessageUseCase'
import { SendMessage } from './SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class SendOwnContactChangeMessage {
constructor(private encryption: EncryptionProviderInterface, private messageServer: AsymmetricMessageServer) {}
export class SendOwnContactChangeMessage implements UseCaseInterface<AsymmetricMessageServerHash> {
constructor(private encryptMessage: EncryptMessage, private sendMessage: SendMessage) {}
async execute(params: {
senderOldKeyPair: PkcKeyPair
@@ -18,7 +18,7 @@ export class SendOwnContactChangeMessage {
senderNewKeyPair: PkcKeyPair
senderNewSigningKeyPair: PkcKeyPair
contact: TrustedContactInterface
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
}): Promise<Result<AsymmetricMessageServerHash>> {
const message: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: {
@@ -28,17 +28,22 @@ export class SendOwnContactChangeMessage {
},
}
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
const encryptedMessage = this.encryptMessage.execute({
message: message,
senderKeyPair: params.senderOldKeyPair,
senderSigningKeyPair: params.senderOldSigningKeyPair,
keys: {
encryption: params.senderOldKeyPair,
signing: params.senderOldSigningKeyPair,
},
recipientPublicKey: params.contact.publicKeySet.encryption,
})
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
const sendMessageResult = await sendMessageUseCase.execute({
if (encryptedMessage.isFailed()) {
return Result.fail(encryptedMessage.getError())
}
const sendMessageResult = await this.sendMessage.execute({
recipientUuid: params.contact.contactUuid,
encryptedMessage,
encryptedMessage: encryptedMessage.getValue(),
replaceabilityIdentifier: undefined,
})