internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -0,0 +1,63 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { HttpServiceInterface } 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'
describe('AsymmetricMessageService', () => {
let service: AsymmetricMessageService
beforeEach(() => {
const http = {} as jest.Mocked<HttpServiceInterface>
http.delete = 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>
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(http, encryption, contacts, items, mutator, sync, eventBus)
})
it('should process incoming messages oldest first', async () => {
const messages: AsymmetricMessageServerHash[] = [
{
uuid: 'newer-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 2,
updated_at_timestamp: 2,
},
{
uuid: 'older-message',
user_uuid: '1',
sender_uuid: '2',
encrypted_message: 'encrypted_message',
created_at_timestamp: 1,
updated_at_timestamp: 1,
},
]
const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } }
service.getTrustedMessagePayload = jest.fn().mockReturnValue(trustedPayloadMock)
const handleTrustedContactShareMessageMock = jest.fn()
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
await service.handleRemoteReceivedAsymmetricMessages(messages)
expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1])
expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0])
})
})

View File

@@ -0,0 +1,187 @@
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError } 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 {
AsymmetricMessageSharedVaultRootKeyChanged,
AsymmetricMessagePayloadType,
AsymmetricMessageSenderKeypairChanged,
AsymmetricMessageTrustedContactShare,
AsymmetricMessagePayload,
AsymmetricMessageSharedVaultMetadataChanged,
VaultListingMutator,
} from '@standardnotes/models'
import { HandleTrustedSharedVaultRootKeyChangedMessage } from './UseCase/HandleTrustedSharedVaultRootKeyChangedMessage'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { SessionEvent } from '../Session/SessionEvent'
import { AsymmetricMessageServer, HttpServiceInterface } 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'
export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface {
private messageServer: AsymmetricMessageServer
constructor(
http: HttpServiceInterface,
private encryption: EncryptionProviderInterface,
private contacts: ContactServiceInterface,
private items: ItemManagerInterface,
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
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)
}
}
public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetOutboundAsymmetricMessages(this.messageServer)
return usecase.execute()
}
public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const usecase = new GetInboundAsymmetricMessages(this.messageServer)
return usecase.execute()
}
async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
return
}
const useCase = new SendOwnContactChangeMessage(this.encryption, this.messageServer)
const contacts = this.contacts.getAllContacts()
for (const contact of contacts) {
if (contact.isMe) {
continue
}
await useCase.execute({
senderOldKeyPair: data.oldKeyPair,
senderOldSigningKeyPair: data.oldSigningKeyPair,
senderNewKeyPair: data.newKeyPair,
senderNewSigningKeyPair: data.newSigningKeyPair,
contact,
})
}
}
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)
for (const message of sortedMessages) {
const trustedMessagePayload = this.getTrustedMessagePayload(message)
if (!trustedMessagePayload) {
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)
}
}
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,
): Promise<void> {
const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: trustedPayload.data.sharedVaultUuid })
if (!vault) {
return
}
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.name = trustedPayload.data.name
mutator.description = trustedPayload.data.description
})
}
async handleTrustedContactShareMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageTrustedContactShare,
): Promise<void> {
await this.contacts.createOrUpdateTrustedContactFromContactShare(trustedPayload.data.trustedContact)
}
private async handleTrustedSenderKeypairChangedMessage(
message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSenderKeypairChanged,
): Promise<void> {
await this.contacts.createOrEditTrustedContact({
contactUuid: message.sender_uuid,
publicKey: trustedPayload.data.newEncryptionPublicKey,
signingPublicKey: trustedPayload.data.newSigningPublicKey,
})
}
private async handleTrustedSharedVaultRootKeyChangedMessage(
_message: AsymmetricMessageServerHash,
trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged,
): Promise<void> {
const useCase = new HandleTrustedSharedVaultRootKeyChangedMessage(
this.mutator,
this.items,
this.sync,
this.encryption,
)
await useCase.execute(trustedPayload)
}
}

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,16 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetInboundAsymmetricMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const response = await this.messageServer.getMessages()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromError(response.data.error)
}
return response.data.messages
}
}

View File

@@ -0,0 +1,16 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class GetOutboundAsymmetricMessages {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
const response = await this.messageServer.getOutboundUserMessages()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromError(response.data.error)
}
return response.data.messages
}
}

View File

@@ -0,0 +1,67 @@
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { HandleTrustedSharedVaultInviteMessage } from './HandleTrustedSharedVaultInviteMessage'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
import { ContentType } from '@standardnotes/common'
import {
AsymmetricMessagePayloadType,
AsymmetricMessageSharedVaultInvite,
KeySystemRootKeyContent,
} from '@standardnotes/models'
describe('HandleTrustedSharedVaultInviteMessage', () => {
let mutatorMock: jest.Mocked<MutatorClientInterface>
let syncServiceMock: jest.Mocked<SyncServiceInterface>
let contactServiceMock: jest.Mocked<ContactServiceInterface>
beforeEach(() => {
mutatorMock = {
createItem: jest.fn(),
} as any
syncServiceMock = {
sync: jest.fn(),
} as any
contactServiceMock = {
createOrEditTrustedContact: jest.fn(),
} as any
})
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 testMessage = {
type: AsymmetricMessagePayloadType.SharedVaultInvite,
data: {
recipientUuid: 'test-recipient-uuid',
rootKey: {
systemIdentifier: 'test-system-identifier',
} as jest.Mocked<KeySystemRootKeyContent>,
metadata: {
name: 'test-name',
},
trustedContacts: [],
},
} as jest.Mocked<AsymmetricMessageSharedVaultInvite>
const sharedVaultUuid = 'test-shared-vault-uuid'
const senderUuid = 'test-sender-uuid'
await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid)
const keySystemRootKeyCallIndex = mutatorMock.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.KeySystemRootKey,
)
const vaultListingCallIndex = mutatorMock.createItem.mock.calls.findIndex(
([contentType]) => contentType === ContentType.VaultListing,
)
expect(keySystemRootKeyCallIndex).toBeLessThan(vaultListingCallIndex)
})
})

View File

@@ -0,0 +1,64 @@
import { ContactServiceInterface } from './../../Contacts/ContactServiceInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import {
KeySystemRootKeyInterface,
AsymmetricMessageSharedVaultInvite,
KeySystemRootKeyContent,
FillItemContent,
FillItemContentSpecialized,
VaultListingContentSpecialized,
KeySystemRootKeyStorageMode,
} from '@standardnotes/models'
import { ContentType } from '@standardnotes/common'
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
export class HandleTrustedSharedVaultInviteMessage {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private contacts: ContactServiceInterface,
) {}
async execute(
message: AsymmetricMessageSharedVaultInvite,
sharedVaultUuid: string,
senderUuid: string,
): Promise<void> {
const { rootKey: rootKeyContent, trustedContacts, metadata } = message.data
const content: VaultListingContentSpecialized = {
systemIdentifier: rootKeyContent.systemIdentifier,
rootKeyParams: rootKeyContent.keyParams,
keyStorageMode: KeySystemRootKeyStorageMode.Synced,
name: metadata.name,
description: metadata.description,
sharing: {
sharedVaultUuid: sharedVaultUuid,
ownerUserUuid: senderUuid,
},
}
await this.mutator.createItem<KeySystemRootKeyInterface>(
ContentType.KeySystemRootKey,
FillItemContent<KeySystemRootKeyContent>(rootKeyContent),
true,
)
await this.mutator.createItem(ContentType.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({
name: contact.name,
contactUuid: contact.contactUuid,
publicKey: contact.publicKeySet.encryption,
signingPublicKey: contact.publicKeySet.signing,
})
}
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
}
}

View File

@@ -0,0 +1,44 @@
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
import {
KeySystemRootKeyInterface,
AsymmetricMessageSharedVaultRootKeyChanged,
FillItemContent,
KeySystemRootKeyContent,
VaultListingMutator,
} from '@standardnotes/models'
import { ContentType } from '@standardnotes/common'
import { GetVaultUseCase } from '../../Vaults/UseCase/GetVault'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
export class HandleTrustedSharedVaultRootKeyChangedMessage {
constructor(
private mutator: MutatorClientInterface,
private items: ItemManagerInterface,
private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface,
) {}
async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> {
const rootKeyContent = message.data.rootKey
await this.mutator.createItem<KeySystemRootKeyInterface>(
ContentType.KeySystemRootKey,
FillItemContent<KeySystemRootKeyContent>(rootKeyContent),
true,
)
const vault = new GetVaultUseCase(this.items).execute({ keySystemIdentifier: rootKeyContent.systemIdentifier })
if (vault) {
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
mutator.rootKeyParams = rootKeyContent.keyParams
})
}
await this.encryption.decryptErroredPayloads()
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
}
}

View File

@@ -0,0 +1,24 @@
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
export class SendAsymmetricMessageUseCase {
constructor(private messageServer: AsymmetricMessageServerInterface) {}
async execute(params: {
recipientUuid: string
encryptedMessage: string
replaceabilityIdentifier: string | undefined
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
const response = await this.messageServer.createMessage({
recipientUuid: params.recipientUuid,
encryptedMessage: params.encryptedMessage,
replaceabilityIdentifier: params.replaceabilityIdentifier,
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromError(response.data.error)
}
return response.data.message
}
}

View File

@@ -0,0 +1,47 @@
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { AsymmetricMessageServerHash, ClientDisplayableError } 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'
export class SendOwnContactChangeMessage {
constructor(private encryption: EncryptionProviderInterface, private messageServer: AsymmetricMessageServer) {}
async execute(params: {
senderOldKeyPair: PkcKeyPair
senderOldSigningKeyPair: PkcKeyPair
senderNewKeyPair: PkcKeyPair
senderNewSigningKeyPair: PkcKeyPair
contact: TrustedContactInterface
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
const message: AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
data: {
recipientUuid: params.contact.contactUuid,
newEncryptionPublicKey: params.senderNewKeyPair.publicKey,
newSigningPublicKey: params.senderNewSigningKeyPair.publicKey,
},
}
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
message: message,
senderKeyPair: params.senderOldKeyPair,
senderSigningKeyPair: params.senderOldSigningKeyPair,
recipientPublicKey: params.contact.publicKeySet.encryption,
})
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
const sendMessageResult = await sendMessageUseCase.execute({
recipientUuid: params.contact.contactUuid,
encryptedMessage,
replaceabilityIdentifier: undefined,
})
return sendMessageResult
}
}