refactor: application dependency management (#2363)
This commit is contained in:
@@ -6,7 +6,9 @@ import { SNFeatureRepo } from '@standardnotes/models'
|
||||
import { ClientDisplayableError, HttpResponse } from '@standardnotes/responses'
|
||||
import { AnyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
export interface ApiServiceInterface extends AbstractService<ApiServiceEvent, ApiServiceEventData>, FilesApiInterface {
|
||||
export interface LegacyApiServiceInterface
|
||||
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
|
||||
FilesApiInterface {
|
||||
isThirdPartyHostUsed(): boolean
|
||||
|
||||
downloadOfflineFeaturesFromRepo(
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
|
||||
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
|
||||
import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface'
|
||||
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
|
||||
import { SyncOptions } from './../Sync/SyncOptions'
|
||||
@@ -34,6 +36,7 @@ import { UserClientInterface } from '../User/UserClientInterface'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface'
|
||||
import { User } from '@standardnotes/responses'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
export interface ApplicationInterface {
|
||||
deinit(mode: DeinitMode, source: DeinitSource): void
|
||||
@@ -107,9 +110,11 @@ export interface ApplicationInterface {
|
||||
get alerts(): AlertService
|
||||
get asymmetric(): AsymmetricMessageServiceInterface
|
||||
get preferences(): PreferenceServiceInterface
|
||||
get events(): InternalEventBusInterface
|
||||
get history(): HistoryServiceInterface
|
||||
get encryption(): EncryptionProviderInterface
|
||||
|
||||
readonly identifier: ApplicationIdentifier
|
||||
readonly platform: Platform
|
||||
deviceInterface: DeviceInterface
|
||||
alertService: AlertService
|
||||
device: DeviceInterface
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
@@ -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> {
|
||||
@@ -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(':')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
|
||||
import { PayloadManagerInterface } from './../Payloads/PayloadManagerInterface'
|
||||
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
|
||||
import { StatusServiceInterface } from './../Status/StatusServiceInterface'
|
||||
import { FilesBackupService } from './BackupService'
|
||||
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
||||
import { LegacyApiServiceInterface } from '../Api/LegacyApiServiceInterface'
|
||||
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { StatusServiceInterface } from '../Status/StatusServiceInterface'
|
||||
import { FilesBackupService } from './FilesBackupService'
|
||||
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { InternalEventBusInterface } from '..'
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { DirectoryManagerInterface, FileBackupsDevice } from '@standardnotes/files'
|
||||
|
||||
describe('backup service', () => {
|
||||
let apiService: ApiServiceInterface
|
||||
let apiService: LegacyApiServiceInterface
|
||||
let itemManager: ItemManagerInterface
|
||||
let syncService: SyncServiceInterface
|
||||
let alertService: AlertService
|
||||
@@ -30,7 +30,7 @@ describe('backup service', () => {
|
||||
let history: HistoryServiceInterface
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<ApiServiceInterface>
|
||||
apiService = {} as jest.Mocked<LegacyApiServiceInterface>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
apiService.downloadFile = jest.fn()
|
||||
@@ -1,6 +1,9 @@
|
||||
import { InternalEventInterface } from './../Internal/InternalEventInterface'
|
||||
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
|
||||
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
import {
|
||||
PayloadEmitSource,
|
||||
FileItem,
|
||||
@@ -34,12 +37,16 @@ import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
const PlaintextBackupsDirectoryName = 'Plaintext Backups'
|
||||
export const TextBackupsDirectoryName = 'Text Backups'
|
||||
export const FileBackupsDirectoryName = 'File Backups'
|
||||
|
||||
export class FilesBackupService extends AbstractService implements BackupServiceInterface {
|
||||
export class FilesBackupService
|
||||
extends AbstractService
|
||||
implements BackupServiceInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private filesObserverDisposer: () => void
|
||||
private notesObserverDisposer: () => void
|
||||
private tagsObserverDisposer: () => void
|
||||
@@ -98,6 +105,15 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
})
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.Launched_10) {
|
||||
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSuperConverter(converter: SuperConverterServiceInterface): void {
|
||||
this.markdownConverter = converter
|
||||
}
|
||||
@@ -143,12 +159,6 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
;(this.session as unknown) = undefined
|
||||
}
|
||||
|
||||
override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
if (stage === ApplicationStage.Launched_10) {
|
||||
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
|
||||
}
|
||||
}
|
||||
|
||||
private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> {
|
||||
if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) {
|
||||
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
|
||||
@@ -10,6 +10,12 @@ import { ChallengeReason } from './Types/ChallengeReason'
|
||||
import { ChallengeObserver } from './Types/ChallengeObserver'
|
||||
|
||||
export interface ChallengeServiceInterface extends AbstractService {
|
||||
sendChallenge: (challenge: ChallengeInterface) => void
|
||||
submitValuesForChallenge(challenge: ChallengeInterface, values: ChallengeValue[]): Promise<void>
|
||||
cancelChallenge(challenge: ChallengeInterface): void
|
||||
|
||||
isPasscodeLocked(): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Resolves when the challenge has been completed.
|
||||
* For non-validated challenges, will resolve when the first value is submitted.
|
||||
|
||||
@@ -1,81 +1,62 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
import { SingletonManagerInterface } from './../Singleton/SingletonManagerInterface'
|
||||
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
|
||||
import { SessionEvent } from './../Session/SessionEvent'
|
||||
import { InternalEventInterface } from './../Internal/InternalEventInterface'
|
||||
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
|
||||
import {
|
||||
TrustedContactContent,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
FillItemContent,
|
||||
TrustedContactMutator,
|
||||
DecryptedItemInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { TrustedContactInterface, TrustedContactMutator, DecryptedItemInterface } from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { UserClientInterface } from '../User/UserClientInterface'
|
||||
import { CollaborationIDData, Version1CollaborationId } from './CollaborationID'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ValidateItemSignerUseCase } from './UseCase/ValidateItemSigner'
|
||||
import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult'
|
||||
import { FindTrustedContactUseCase } from './UseCase/FindTrustedContact'
|
||||
import { SelfContactManager } from './Managers/SelfContactManager'
|
||||
import { CreateOrEditTrustedContactUseCase } from './UseCase/CreateOrEditTrustedContact'
|
||||
import { UpdateTrustedContactUseCase } from './UseCase/UpdateTrustedContact'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ValidateItemSigner } from './UseCase/ValidateItemSigner'
|
||||
import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
|
||||
import { FindContact } from './UseCase/FindContact'
|
||||
import { SelfContactManager } from './SelfContactManager'
|
||||
import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
|
||||
import { EditContact } from './UseCase/EditContact'
|
||||
import { GetAllContacts } from './UseCase/GetAllContacts'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
export class ContactService
|
||||
extends AbstractService<ContactServiceEvent>
|
||||
implements ContactServiceInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private selfContactManager: SelfContactManager
|
||||
|
||||
constructor(
|
||||
private sync: SyncServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private session: SessionsClientInterface,
|
||||
private crypto: PureCryptoInterface,
|
||||
private user: UserClientInterface,
|
||||
private selfContactManager: SelfContactManager,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
singletons: SingletonManagerInterface,
|
||||
private _findContact: FindContact,
|
||||
private _getAllContacts: GetAllContacts,
|
||||
private _createOrEditContact: CreateOrEditContact,
|
||||
private _editContact: EditContact,
|
||||
private _validateItemSigner: ValidateItemSigner,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
this.selfContactManager = new SelfContactManager(sync, items, mutator, session, singletons)
|
||||
|
||||
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
|
||||
}
|
||||
|
||||
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
await super.handleApplicationStage(stage)
|
||||
await this.selfContactManager.handleApplicationStage(stage)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SessionEvent.UserKeyPairChanged) {
|
||||
const data = event.payload as UserKeyPairChangedEventData
|
||||
|
||||
await this.selfContactManager.updateWithNewPublicKeySet({
|
||||
encryption: data.newKeyPair.publicKey,
|
||||
signing: data.newSigningKeyPair.publicKey,
|
||||
encryption: data.current.encryption.publicKey,
|
||||
signing: data.current.signing.publicKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private get userUuid(): string {
|
||||
return this.session.getSureUser().uuid
|
||||
}
|
||||
|
||||
getSelfContact(): TrustedContactInterface | undefined {
|
||||
return this.selfContactManager.selfContact
|
||||
}
|
||||
@@ -170,38 +151,11 @@ export class ContactService
|
||||
contact: TrustedContactInterface,
|
||||
params: { name: string; publicKey: string; signingPublicKey: string },
|
||||
): Promise<TrustedContactInterface> {
|
||||
const usecase = new UpdateTrustedContactUseCase(this.mutator, this.sync)
|
||||
const updatedContact = await usecase.execute(contact, params)
|
||||
const updatedContact = await this._editContact.execute(contact, params)
|
||||
|
||||
return updatedContact
|
||||
}
|
||||
|
||||
async createOrUpdateTrustedContactFromContactShare(
|
||||
data: TrustedContactContentSpecialized,
|
||||
): Promise<TrustedContactInterface> {
|
||||
if (data.contactUuid === this.userUuid) {
|
||||
throw new Error('Cannot receive self from contact share')
|
||||
}
|
||||
|
||||
let contact = this.findTrustedContact(data.contactUuid)
|
||||
if (contact) {
|
||||
contact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(contact, (mutator) => {
|
||||
mutator.name = data.name
|
||||
mutator.replacePublicKeySet(data.publicKeySet)
|
||||
})
|
||||
} else {
|
||||
contact = await this.mutator.createItem<TrustedContactInterface>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
FillItemContent<TrustedContactContent>(data),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return contact
|
||||
}
|
||||
|
||||
async createOrEditTrustedContact(params: {
|
||||
name?: string
|
||||
contactUuid: string
|
||||
@@ -209,8 +163,7 @@ export class ContactService
|
||||
signingPublicKey: string
|
||||
isMe?: boolean
|
||||
}): Promise<TrustedContactInterface | undefined> {
|
||||
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync)
|
||||
const contact = await usecase.execute(params)
|
||||
const contact = await this._createOrEditContact.execute(params)
|
||||
return contact
|
||||
}
|
||||
|
||||
@@ -224,12 +177,15 @@ export class ContactService
|
||||
}
|
||||
|
||||
getAllContacts(): TrustedContactInterface[] {
|
||||
return this.items.getItems(ContentType.TYPES.TrustedContact)
|
||||
return this._getAllContacts.execute().getValue()
|
||||
}
|
||||
|
||||
findTrustedContact(userUuid: string): TrustedContactInterface | undefined {
|
||||
const usecase = new FindTrustedContactUseCase(this.items)
|
||||
return usecase.execute({ userUuid })
|
||||
const result = this._findContact.execute({ userUuid })
|
||||
if (result.isFailed()) {
|
||||
return undefined
|
||||
}
|
||||
return result.getValue()
|
||||
}
|
||||
|
||||
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
|
||||
@@ -249,16 +205,23 @@ export class ContactService
|
||||
})
|
||||
}
|
||||
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
const usecase = new ValidateItemSignerUseCase(this.items)
|
||||
return usecase.execute(item)
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult {
|
||||
return this._validateItemSigner.execute(item)
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
this.selfContactManager.deinit()
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
;(this.session as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this.user as unknown) = undefined
|
||||
;(this.selfContactManager as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this._findContact as unknown) = undefined
|
||||
;(this._getAllContacts as unknown) = undefined
|
||||
;(this._createOrEditContact as unknown) = undefined
|
||||
;(this._editContact as unknown) = undefined
|
||||
;(this._validateItemSigner as unknown) = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { DecryptedItemInterface, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
|
||||
import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult'
|
||||
import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
|
||||
|
||||
export enum ContactServiceEvent {}
|
||||
|
||||
@@ -26,7 +22,6 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
|
||||
publicKey: string
|
||||
signingPublicKey: string
|
||||
}): Promise<TrustedContactInterface | undefined>
|
||||
createOrUpdateTrustedContactFromContactShare(data: TrustedContactContentSpecialized): Promise<TrustedContactInterface>
|
||||
editTrustedContactFromCollaborationID(
|
||||
contact: TrustedContactInterface,
|
||||
params: { name: string; collaborationID: string },
|
||||
@@ -39,5 +34,5 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
|
||||
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
|
||||
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
|
||||
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { InternalFeature } from './../../InternalFeatures/InternalFeature'
|
||||
import { InternalFeatureService } from '../../InternalFeatures/InternalFeatureService'
|
||||
import { ApplicationStage } from './../../Application/ApplicationStage'
|
||||
import { SingletonManagerInterface } from './../../Singleton/SingletonManagerInterface'
|
||||
import { SyncEvent } from './../../Event/SyncEvent'
|
||||
import { SessionsClientInterface } from '../../Session/SessionsClientInterface'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { ApplicationStageChangedEventPayload } from './../Event/ApplicationStageChangedEventPayload'
|
||||
import { ApplicationEvent } from './../Event/ApplicationEvent'
|
||||
import { InternalEventInterface } from './../Internal/InternalEventInterface'
|
||||
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
|
||||
import { InternalFeature } from '../InternalFeatures/InternalFeature'
|
||||
import { InternalFeatureService } from '../InternalFeatures/InternalFeatureService'
|
||||
import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
import { SingletonManagerInterface } from '../Singleton/SingletonManagerInterface'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import {
|
||||
ContactPublicKeySet,
|
||||
FillItemContent,
|
||||
@@ -14,23 +17,25 @@ import {
|
||||
TrustedContactContent,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
PortablePublicKeySet,
|
||||
} from '@standardnotes/models'
|
||||
import { CreateOrEditTrustedContactUseCase } from '../UseCase/CreateOrEditTrustedContact'
|
||||
import { PublicKeySet } from '@standardnotes/encryption'
|
||||
import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class SelfContactManager {
|
||||
const SelfContactName = 'Me'
|
||||
|
||||
export class SelfContactManager implements InternalEventHandlerInterface {
|
||||
public selfContact?: TrustedContactInterface
|
||||
|
||||
private isReloadingSelfContact = false
|
||||
private eventDisposers: (() => void)[] = []
|
||||
|
||||
constructor(
|
||||
private sync: SyncServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
sync: SyncServiceInterface,
|
||||
items: ItemManagerInterface,
|
||||
private session: SessionsClientInterface,
|
||||
private singletons: SingletonManagerInterface,
|
||||
private createOrEditContact: CreateOrEditContact,
|
||||
) {
|
||||
this.eventDisposers.push(
|
||||
sync.addEventObserver((event) => {
|
||||
@@ -43,11 +48,26 @@ export class SelfContactManager {
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
this.eventDisposers.push(
|
||||
items.addObserver(ContentType.TYPES.TrustedContact, () => {
|
||||
const updatedReference = this.singletons.findSingleton<TrustedContact>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
TrustedContact.singletonPredicate,
|
||||
)
|
||||
if (updatedReference) {
|
||||
this.selfContact = updatedReference
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
if (stage === ApplicationStage.LoadedDatabase_12) {
|
||||
this.loadSelfContactFromDatabase()
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.LoadedDatabase_12) {
|
||||
this.loadSelfContactFromDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +82,7 @@ export class SelfContactManager {
|
||||
)
|
||||
}
|
||||
|
||||
public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) {
|
||||
public async updateWithNewPublicKeySet(publicKeySet: PortablePublicKeySet) {
|
||||
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||
return
|
||||
}
|
||||
@@ -71,9 +91,8 @@ export class SelfContactManager {
|
||||
return
|
||||
}
|
||||
|
||||
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync)
|
||||
await usecase.execute({
|
||||
name: 'Me',
|
||||
await this.createOrEditContact.execute({
|
||||
name: SelfContactName,
|
||||
contactUuid: this.selfContact.contactUuid,
|
||||
publicKey: publicKeySet.encryption,
|
||||
signingPublicKey: publicKeySet.signing,
|
||||
@@ -104,13 +123,12 @@ export class SelfContactManager {
|
||||
this.isReloadingSelfContact = true
|
||||
|
||||
const content: TrustedContactContentSpecialized = {
|
||||
name: 'Me',
|
||||
name: SelfContactName,
|
||||
isMe: true,
|
||||
contactUuid: this.session.getSureUser().uuid,
|
||||
publicKeySet: ContactPublicKeySet.FromJson({
|
||||
encryption: this.session.getPublicKey(),
|
||||
signing: this.session.getSigningPublicKey(),
|
||||
isRevoked: false,
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
}
|
||||
@@ -126,10 +144,8 @@ export class SelfContactManager {
|
||||
|
||||
deinit() {
|
||||
this.eventDisposers.forEach((disposer) => disposer())
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.mutator as unknown) = undefined
|
||||
;(this.session as unknown) = undefined
|
||||
;(this.singletons as unknown) = undefined
|
||||
;(this.createOrEditContact as unknown) = undefined
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
import {
|
||||
ContactPublicKeySet,
|
||||
FillItemContent,
|
||||
@@ -8,16 +7,17 @@ import {
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { FindTrustedContactUseCase } from './FindTrustedContact'
|
||||
import { FindContact } from './FindContact'
|
||||
import { UnknownContactName } from '../UnknownContactName'
|
||||
import { UpdateTrustedContactUseCase } from './UpdateTrustedContact'
|
||||
import { EditContact } from './EditContact'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class CreateOrEditTrustedContactUseCase {
|
||||
export class CreateOrEditContact {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private findContact: FindContact,
|
||||
private editContact: EditContact,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
@@ -27,13 +27,14 @@ export class CreateOrEditTrustedContactUseCase {
|
||||
signingPublicKey: string
|
||||
isMe?: boolean
|
||||
}): Promise<TrustedContactInterface | undefined> {
|
||||
const findUsecase = new FindTrustedContactUseCase(this.items)
|
||||
const existingContact = findUsecase.execute({ userUuid: params.contactUuid })
|
||||
const existingContact = this.findContact.execute({ userUuid: params.contactUuid })
|
||||
|
||||
if (existingContact) {
|
||||
const updateUsecase = new UpdateTrustedContactUseCase(this.mutator, this.sync)
|
||||
await updateUsecase.execute(existingContact, { ...params, name: params.name ?? existingContact.name })
|
||||
return existingContact
|
||||
if (!existingContact.isFailed()) {
|
||||
await this.editContact.execute(existingContact.getValue(), {
|
||||
...params,
|
||||
name: params.name ?? existingContact.getValue().name,
|
||||
})
|
||||
return existingContact.getValue()
|
||||
}
|
||||
|
||||
const content: TrustedContactContentSpecialized = {
|
||||
@@ -41,7 +42,6 @@ export class CreateOrEditTrustedContactUseCase {
|
||||
publicKeySet: ContactPublicKeySet.FromJson({
|
||||
encryption: params.publicKey,
|
||||
signing: params.signingPublicKey,
|
||||
isRevoked: false,
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
contactUuid: params.contactUuid,
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models'
|
||||
|
||||
export class UpdateTrustedContactUseCase {
|
||||
export class EditContact {
|
||||
constructor(private mutator: MutatorClientInterface, private sync: SyncServiceInterface) {}
|
||||
|
||||
async execute(
|
||||
38
packages/services/src/Domain/Contacts/UseCase/FindContact.ts
Normal file
38
packages/services/src/Domain/Contacts/UseCase/FindContact.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Predicate, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { FindContactQuery } from './Types/FindContactQuery'
|
||||
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class FindContact implements SyncUseCaseInterface<TrustedContactInterface> {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(query: FindContactQuery): Result<TrustedContactInterface> {
|
||||
if ('userUuid' in query && query.userUuid) {
|
||||
const contact = this.items.itemsMatchingPredicate<TrustedContactInterface>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
|
||||
)[0]
|
||||
|
||||
if (contact) {
|
||||
return Result.ok(contact)
|
||||
} else {
|
||||
return Result.fail('Contact not found')
|
||||
}
|
||||
}
|
||||
|
||||
if ('signingPublicKey' in query && query.signingPublicKey) {
|
||||
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
|
||||
const contact = allContacts.find((contact) =>
|
||||
contact.hasCurrentOrPreviousSigningPublicKey(query.signingPublicKey),
|
||||
)
|
||||
|
||||
if (contact) {
|
||||
return Result.ok(contact)
|
||||
} else {
|
||||
return Result.fail('Contact not found')
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Invalid query')
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Predicate, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { FindContactQuery } from './FindContactQuery'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class FindTrustedContactUseCase {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(query: FindContactQuery): TrustedContactInterface | undefined {
|
||||
if ('userUuid' in query && query.userUuid) {
|
||||
return this.items.itemsMatchingPredicate<TrustedContactInterface>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
|
||||
)[0]
|
||||
}
|
||||
|
||||
if ('signingPublicKey' in query && query.signingPublicKey) {
|
||||
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
|
||||
return allContacts.find((contact) => contact.isSigningKeyTrusted(query.signingPublicKey))
|
||||
}
|
||||
|
||||
if ('publicKey' in query && query.publicKey) {
|
||||
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TYPES.TrustedContact)
|
||||
return allContacts.find((contact) => contact.isPublicKeyTrusted(query.publicKey))
|
||||
}
|
||||
|
||||
throw new Error('Invalid query')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { TrustedContactInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class GetAllContacts implements SyncUseCaseInterface<TrustedContactInterface[]> {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(): Result<TrustedContactInterface[]> {
|
||||
return Result.ok(this.items.getItems(ContentType.TYPES.TrustedContact))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { ReuploadAllInvites } from '../../SharedVaults/UseCase/ReuploadAllInvites'
|
||||
import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages'
|
||||
|
||||
export class HandleKeyPairChange implements UseCaseInterface<void> {
|
||||
constructor(private reuploadAllInvites: ReuploadAllInvites, private resendAllMessages: ResendAllMessages) {}
|
||||
|
||||
async execute(dto: {
|
||||
newKeys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
previousKeys?: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}): Promise<Result<void>> {
|
||||
await this.reuploadAllInvites.execute({
|
||||
keys: dto.newKeys,
|
||||
previousKeys: dto.previousKeys,
|
||||
})
|
||||
|
||||
await this.resendAllMessages.execute({
|
||||
keys: dto.newKeys,
|
||||
previousKeys: dto.previousKeys,
|
||||
})
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
import {
|
||||
FillItemContent,
|
||||
MutationType,
|
||||
PayloadEmitSource,
|
||||
TrustedContactContent,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
TrustedContactMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { FindContact } from './FindContact'
|
||||
import { ContentType, Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class ReplaceContactData implements UseCaseInterface<TrustedContactInterface> {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private findContact: FindContact,
|
||||
) {}
|
||||
|
||||
async execute(data: TrustedContactContentSpecialized): Promise<Result<TrustedContactInterface>> {
|
||||
const contactResult = this.findContact.execute({ userUuid: data.contactUuid })
|
||||
if (contactResult.isFailed()) {
|
||||
const newContact = await this.mutator.createItem<TrustedContactInterface>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
FillItemContent<TrustedContactContent>(data),
|
||||
true,
|
||||
)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return Result.ok(newContact)
|
||||
}
|
||||
|
||||
const existingContact = contactResult.getValue()
|
||||
if (existingContact.isMe) {
|
||||
return Result.fail('Cannot replace data for me contact')
|
||||
}
|
||||
|
||||
const updatedContact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(
|
||||
existingContact,
|
||||
(mutator) => {
|
||||
mutator.name = data.name
|
||||
mutator.replacePublicKeySet(data.publicKeySet)
|
||||
},
|
||||
MutationType.UpdateUserTimestamps,
|
||||
PayloadEmitSource.RemoteRetrieved,
|
||||
)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return Result.ok(updatedContact)
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export type FindContactQuery = { userUuid: string } | { signingPublicKey: string } | { publicKey: string }
|
||||
export type FindContactQuery = { userUuid: string } | { signingPublicKey: string }
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum ItemSignatureValidationResult {
|
||||
NotApplicable = 'NotApplicable',
|
||||
Trusted = 'Trusted',
|
||||
SignedWithNonCurrentKey = 'SignedWithNonCurrentKey',
|
||||
NotTrusted = 'NotTrusted',
|
||||
}
|
||||
@@ -2,21 +2,26 @@ import {
|
||||
DecryptedItemInterface,
|
||||
PayloadSource,
|
||||
PersistentSignatureData,
|
||||
PublicKeyTrustStatus,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { ValidateItemSignerUseCase } from './ValidateItemSigner'
|
||||
import { ValidateItemSigner } from './ValidateItemSigner'
|
||||
import { FindContact } from './FindContact'
|
||||
import { ItemSignatureValidationResult } from './Types/ItemSignatureValidationResult'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('validate item signer use case', () => {
|
||||
let usecase: ValidateItemSignerUseCase
|
||||
let items: ItemManagerInterface
|
||||
let usecase: ValidateItemSigner
|
||||
let findContact: FindContact
|
||||
|
||||
const trustedContact = {} as jest.Mocked<TrustedContactInterface>
|
||||
trustedContact.isSigningKeyTrusted = jest.fn().mockReturnValue(true)
|
||||
trustedContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
|
||||
|
||||
beforeEach(() => {
|
||||
items = {} as jest.Mocked<ItemManagerInterface>
|
||||
usecase = new ValidateItemSignerUseCase(items)
|
||||
findContact = {} as jest.Mocked<FindContact>
|
||||
findContact.execute = jest.fn().mockReturnValue(Result.ok(trustedContact))
|
||||
|
||||
usecase = new ValidateItemSigner(findContact)
|
||||
})
|
||||
|
||||
const createItem = (params: {
|
||||
@@ -42,7 +47,7 @@ describe('validate item signer use case', () => {
|
||||
describe('has last edited by uuid', () => {
|
||||
describe('trusted contact not found', () => {
|
||||
beforeEach(() => {
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([])
|
||||
findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
|
||||
})
|
||||
|
||||
it('should return invalid signing is required', () => {
|
||||
@@ -53,7 +58,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return not applicable signing is not required', () => {
|
||||
@@ -64,15 +69,11 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
})
|
||||
|
||||
describe('trusted contact found for last editor', () => {
|
||||
beforeEach(() => {
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact])
|
||||
})
|
||||
|
||||
describe('does not have signature data', () => {
|
||||
it('should return not applicable if the item was just recently created', () => {
|
||||
const item = createItem({
|
||||
@@ -83,7 +84,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
|
||||
it('should return not applicable if the item was just recently saved', () => {
|
||||
@@ -95,7 +96,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
|
||||
it('should return invalid if signing is required', () => {
|
||||
@@ -106,7 +107,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
@@ -117,7 +118,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,7 +134,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
@@ -146,7 +147,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -164,7 +165,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
|
||||
@@ -180,10 +181,10 @@ describe('validate item signer use case', () => {
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([])
|
||||
findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
|
||||
@@ -199,10 +200,10 @@ describe('validate item signer use case', () => {
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact])
|
||||
findContact.execute = jest.fn().mockReturnValue(Result.ok(trustedContact))
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('yes')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.Trusted)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -220,7 +221,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
|
||||
it('should return not applicable if the item was just recently saved', () => {
|
||||
@@ -232,7 +233,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
|
||||
it('should return invalid if signing is required', () => {
|
||||
@@ -243,7 +244,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
@@ -254,7 +255,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -270,7 +271,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
@@ -283,7 +284,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotApplicable)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -301,7 +302,7 @@ describe('validate item signer use case', () => {
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
|
||||
@@ -317,10 +318,10 @@ describe('validate item signer use case', () => {
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.getItems = jest.fn().mockReturnValue([])
|
||||
findContact.execute = jest.fn().mockReturnValue(Result.fail('Not found'))
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.NotTrusted)
|
||||
})
|
||||
|
||||
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
|
||||
@@ -336,10 +337,8 @@ describe('validate item signer use case', () => {
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.getItems = jest.fn().mockReturnValue([trustedContact])
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('yes')
|
||||
expect(result).toEqual(ItemSignatureValidationResult.Trusted)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { doesPayloadRequireSigning } from '@standardnotes/encryption/src/Domain/Operator/004/V004AlgorithmHelpers'
|
||||
import { DecryptedItemInterface, PayloadSource } from '@standardnotes/models'
|
||||
import { ValidateItemSignerResult } from './ValidateItemSignerResult'
|
||||
import { FindTrustedContactUseCase } from './FindTrustedContact'
|
||||
import { DecryptedItemInterface, PayloadSource, PublicKeyTrustStatus } from '@standardnotes/models'
|
||||
import { ItemSignatureValidationResult } from './Types/ItemSignatureValidationResult'
|
||||
import { FindContact } from './FindContact'
|
||||
|
||||
export class ValidateItemSignerUseCase {
|
||||
private findContactUseCase = new FindTrustedContactUseCase(this.items)
|
||||
export class ValidateItemSigner {
|
||||
constructor(private findContact: FindContact) {}
|
||||
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
execute(item: DecryptedItemInterface): ItemSignatureValidationResult {
|
||||
const uuidOfLastEditor = item.last_edited_by_uuid
|
||||
if (uuidOfLastEditor) {
|
||||
return this.validateSignatureWithLastEditedByUuid(item, uuidOfLastEditor)
|
||||
@@ -29,15 +26,15 @@ export class ValidateItemSignerUseCase {
|
||||
private validateSignatureWithLastEditedByUuid(
|
||||
item: DecryptedItemInterface,
|
||||
uuidOfLastEditor: string,
|
||||
): ValidateItemSignerResult {
|
||||
): ItemSignatureValidationResult {
|
||||
const requiresSignature = doesPayloadRequireSigning(item)
|
||||
|
||||
const trustedContact = this.findContactUseCase.execute({ userUuid: uuidOfLastEditor })
|
||||
if (!trustedContact) {
|
||||
const trustedContact = this.findContact.execute({ userUuid: uuidOfLastEditor })
|
||||
if (trustedContact.isFailed()) {
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
} else {
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,38 +43,41 @@ export class ValidateItemSignerUseCase {
|
||||
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
|
||||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
|
||||
) {
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
|
||||
const signatureData = item.signatureData
|
||||
if (!signatureData.result) {
|
||||
if (signatureData.required) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
|
||||
const signatureResult = signatureData.result
|
||||
|
||||
if (!signatureResult.passes) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
|
||||
const signerPublicKey = signatureResult.publicKey
|
||||
|
||||
if (trustedContact.isSigningKeyTrusted(signerPublicKey)) {
|
||||
return 'yes'
|
||||
const trustStatus = trustedContact.getValue().getTrustStatusForSigningPublicKey(signerPublicKey)
|
||||
if (trustStatus === PublicKeyTrustStatus.Trusted) {
|
||||
return ItemSignatureValidationResult.Trusted
|
||||
} else if (trustStatus === PublicKeyTrustStatus.Previous) {
|
||||
return ItemSignatureValidationResult.SignedWithNonCurrentKey
|
||||
}
|
||||
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
|
||||
private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ItemSignatureValidationResult {
|
||||
const requiresSignature = doesPayloadRequireSigning(item)
|
||||
|
||||
if (!item.signatureData) {
|
||||
@@ -85,38 +85,45 @@ export class ValidateItemSignerUseCase {
|
||||
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
|
||||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
|
||||
) {
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
|
||||
const signatureData = item.signatureData
|
||||
if (!signatureData.result) {
|
||||
if (signatureData.required) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
return 'not-applicable'
|
||||
return ItemSignatureValidationResult.NotApplicable
|
||||
}
|
||||
|
||||
const signatureResult = signatureData.result
|
||||
|
||||
if (!signatureResult.passes) {
|
||||
return 'no'
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
|
||||
const signerPublicKey = signatureResult.publicKey
|
||||
|
||||
const trustedContact = this.findContactUseCase.execute({ signingPublicKey: signerPublicKey })
|
||||
const trustedContact = this.findContact.execute({ signingPublicKey: signerPublicKey })
|
||||
|
||||
if (trustedContact) {
|
||||
return 'yes'
|
||||
if (trustedContact.isFailed()) {
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
|
||||
return 'no'
|
||||
const trustStatus = trustedContact.getValue().getTrustStatusForSigningPublicKey(signerPublicKey)
|
||||
if (trustStatus === PublicKeyTrustStatus.Trusted) {
|
||||
return ItemSignatureValidationResult.Trusted
|
||||
} else if (trustStatus === PublicKeyTrustStatus.Previous) {
|
||||
return ItemSignatureValidationResult.SignedWithNonCurrentKey
|
||||
}
|
||||
|
||||
return ItemSignatureValidationResult.NotTrusted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type ValidateItemSignerResult = 'not-applicable' | 'yes' | 'no'
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
AsymmetricSignatureVerificationDetachedResult,
|
||||
SNRootKeyParams,
|
||||
KeyedDecryptionSplit,
|
||||
KeyedEncryptionSplit,
|
||||
ItemAuthenticatedData,
|
||||
AsymmetricallyEncryptedString,
|
||||
} from '@standardnotes/encryption'
|
||||
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
KeySystemIdentifier,
|
||||
KeySystemItemsKeyInterface,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyParamsInterface,
|
||||
PortablePublicKeySet,
|
||||
} from '@standardnotes/models'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export interface EncryptionProviderInterface {
|
||||
initialize(): Promise<void>
|
||||
|
||||
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
|
||||
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
|
||||
decryptSplitSingle<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(
|
||||
split: KeyedDecryptionSplit,
|
||||
): Promise<P | EncryptedPayloadInterface>
|
||||
decryptSplit<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(
|
||||
split: KeyedDecryptionSplit,
|
||||
): Promise<(P | EncryptedPayloadInterface)[]>
|
||||
|
||||
getEmbeddedPayloadAuthenticatedData<D extends ItemAuthenticatedData>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): D | undefined
|
||||
getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
|
||||
|
||||
supportedVersions(): ProtocolVersion[]
|
||||
isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean
|
||||
platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean
|
||||
|
||||
getPasswordCreatedDate(): Date | undefined
|
||||
getEncryptionDisplayName(): Promise<string>
|
||||
upgradeAvailable(): Promise<boolean>
|
||||
|
||||
createEncryptedBackupFile(): Promise<BackupFile>
|
||||
createDecryptedBackupFile(): BackupFile
|
||||
|
||||
getUserVersion(): ProtocolVersion | undefined
|
||||
hasAccount(): boolean
|
||||
hasPasscode(): boolean
|
||||
removePasscode(): Promise<void>
|
||||
validateAccountPassword(password: string): Promise<
|
||||
| {
|
||||
valid: true
|
||||
artifacts: {
|
||||
rootKey: RootKeyInterface
|
||||
}
|
||||
}
|
||||
| {
|
||||
valid: boolean
|
||||
}
|
||||
>
|
||||
|
||||
decryptErroredPayloads(): Promise<void>
|
||||
deleteWorkspaceSpecificKeyStateFromDevice(): Promise<void>
|
||||
|
||||
unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void>
|
||||
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
|
||||
computeWrappingKey(passcode: string): Promise<RootKeyInterface>
|
||||
hasRootKeyEncryptionSource(): boolean
|
||||
createRootKey<K extends RootKeyInterface>(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
version?: ProtocolVersion,
|
||||
): Promise<K>
|
||||
getRootKeyParams(): SNRootKeyParams | undefined
|
||||
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
|
||||
|
||||
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
|
||||
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
|
||||
getSureDefaultItemsKey(): ItemsKeyInterface
|
||||
|
||||
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
|
||||
|
||||
createUserInputtedKeySystemRootKey(dto: {
|
||||
systemIdentifier: KeySystemIdentifier
|
||||
userInputtedPassword: string
|
||||
}): KeySystemRootKeyInterface
|
||||
|
||||
deriveUserInputtedKeySystemRootKey(dto: {
|
||||
keyParams: KeySystemRootKeyParamsInterface
|
||||
userInputtedPassword: string
|
||||
}): KeySystemRootKeyInterface
|
||||
|
||||
createKeySystemItemsKey(
|
||||
uuid: string,
|
||||
keySystemIdentifier: KeySystemIdentifier,
|
||||
sharedVaultUuid: string | undefined,
|
||||
rootKeyToken: string,
|
||||
): KeySystemItemsKeyInterface
|
||||
|
||||
getKeyPair(): PkcKeyPair
|
||||
getSigningKeyPair(): PkcKeyPair
|
||||
|
||||
asymmetricSignatureVerifyDetached(
|
||||
encryptedString: AsymmetricallyEncryptedString,
|
||||
): AsymmetricSignatureVerificationDetachedResult
|
||||
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: string): PortablePublicKeySet
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FindDefaultItemsKey } from './UseCase/ItemsKey/FindDefaultItemsKey'
|
||||
import { InternalEventInterface } from './../Internal/InternalEventInterface'
|
||||
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
@@ -5,27 +6,22 @@ import {
|
||||
CreateAnyKeyParams,
|
||||
CreateEncryptionSplitWithKeyLookup,
|
||||
encryptedInputParametersFromPayload,
|
||||
EncryptionProviderInterface,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
FindPayloadInDecryptionSplit,
|
||||
FindPayloadInEncryptionSplit,
|
||||
isErrorDecryptingParameters,
|
||||
ItemAuthenticatedData,
|
||||
KeyedDecryptionSplit,
|
||||
KeyedEncryptionSplit,
|
||||
KeyMode,
|
||||
LegacyAttachedData,
|
||||
OperatorManager,
|
||||
RootKeyEncryptedAuthenticatedData,
|
||||
SplitPayloadsByEncryptionType,
|
||||
V001Algorithm,
|
||||
V002Algorithm,
|
||||
PublicKeySet,
|
||||
EncryptedOutputParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
AsymmetricSignatureVerificationDetachedResult,
|
||||
AsymmetricallyEncryptedString,
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
BackupFile,
|
||||
@@ -42,13 +38,11 @@ import {
|
||||
RootKeyInterface,
|
||||
KeySystemItemsKeyInterface,
|
||||
KeySystemIdentifier,
|
||||
AsymmetricMessagePayload,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyParamsInterface,
|
||||
TrustedContactInterface,
|
||||
PortablePublicKeySet,
|
||||
RootKeyParamsInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
extendArray,
|
||||
@@ -60,7 +54,6 @@ import {
|
||||
} from '@standardnotes/utils'
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
ApplicationIdentifier,
|
||||
compareVersions,
|
||||
isVersionLessThanOrEqualTo,
|
||||
KeyParamsOrigination,
|
||||
@@ -70,28 +63,27 @@ import {
|
||||
} from '@standardnotes/common'
|
||||
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemsEncryptionService } from './ItemsEncryption'
|
||||
import { ItemsEncryptionService } from '../ItemsEncryption/ItemsEncryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { DeviceInterface } from '../Device/DeviceInterface'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
import { DecryptBackupFileUseCase } from './DecryptBackupFileUseCase'
|
||||
import { EncryptionServiceEvent } from './EncryptionServiceEvent'
|
||||
import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters'
|
||||
import { RootKeyManager } from './RootKey/RootKeyManager'
|
||||
import { RootKeyManagerEvent } from './RootKey/RootKeyManagerEvent'
|
||||
import { CreateNewItemsKeyWithRollbackUseCase } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
|
||||
import { DecryptErroredRootPayloadsUseCase } from './UseCase/RootEncryption/DecryptErroredPayloads'
|
||||
import { CreateNewDefaultItemsKeyUseCase } from './UseCase/ItemsKey/CreateNewDefaultItemsKey'
|
||||
import { RootKeyDecryptPayloadUseCase } from './UseCase/RootEncryption/DecryptPayload'
|
||||
import { RootKeyDecryptPayloadWithKeyLookupUseCase } from './UseCase/RootEncryption/DecryptPayloadWithKeyLookup'
|
||||
import { RootKeyEncryptPayloadWithKeyLookupUseCase } from './UseCase/RootEncryption/EncryptPayloadWithKeyLookup'
|
||||
import { RootKeyEncryptPayloadUseCase } from './UseCase/RootEncryption/EncryptPayload'
|
||||
import { ValidateAccountPasswordResult } from './RootKey/ValidateAccountPasswordResult'
|
||||
import { ValidatePasscodeResult } from './RootKey/ValidatePasscodeResult'
|
||||
import { RootKeyManager } from '../RootKeyManager/RootKeyManager'
|
||||
import { RootKeyManagerEvent } from '../RootKeyManager/RootKeyManagerEvent'
|
||||
import { CreateNewItemsKeyWithRollback } from './UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
|
||||
import { DecryptErroredTypeAPayloads } from './UseCase/TypeA/DecryptErroredPayloads'
|
||||
import { CreateNewDefaultItemsKey } from './UseCase/ItemsKey/CreateNewDefaultItemsKey'
|
||||
import { DecryptTypeAPayload } from './UseCase/TypeA/DecryptPayload'
|
||||
import { DecryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/DecryptPayloadWithKeyLookup'
|
||||
import { EncryptTypeAPayloadWithKeyLookup } from './UseCase/TypeA/EncryptPayloadWithKeyLookup'
|
||||
import { EncryptTypeAPayload } from './UseCase/TypeA/EncryptPayload'
|
||||
import { ValidateAccountPasswordResult } from '../RootKeyManager/ValidateAccountPasswordResult'
|
||||
import { ValidatePasscodeResult } from '../RootKeyManager/ValidatePasscodeResult'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from './EncryptionProviderInterface'
|
||||
import { KeyMode } from '../RootKeyManager/KeyMode'
|
||||
|
||||
/**
|
||||
* The encryption service is responsible for the encryption and decryption of payloads, and
|
||||
@@ -124,40 +116,28 @@ export class EncryptionService
|
||||
extends AbstractService<EncryptionServiceEvent>
|
||||
implements EncryptionProviderInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private operators: OperatorManager
|
||||
private readonly itemsEncryption: ItemsEncryptionService
|
||||
private readonly rootKeyManager: RootKeyManager
|
||||
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private payloads: PayloadManagerInterface,
|
||||
public device: DeviceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
public readonly keys: KeySystemKeyManagerInterface,
|
||||
identifier: ApplicationIdentifier,
|
||||
public crypto: PureCryptoInterface,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private itemsEncryption: ItemsEncryptionService,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
private crypto: PureCryptoInterface,
|
||||
private _createNewItemsKeyWithRollback: CreateNewItemsKeyWithRollback,
|
||||
private _findDefaultItemsKey: FindDefaultItemsKey,
|
||||
private _decryptErroredRootPayloads: DecryptErroredTypeAPayloads,
|
||||
private _rootKeyEncryptPayloadWithKeyLookup: EncryptTypeAPayloadWithKeyLookup,
|
||||
private _rootKeyEncryptPayload: EncryptTypeAPayload,
|
||||
private _rootKeyDecryptPayload: DecryptTypeAPayload,
|
||||
private _rootKeyDecryptPayloadWithKeyLookup: DecryptTypeAPayloadWithKeyLookup,
|
||||
private _createDefaultItemsKey: CreateNewDefaultItemsKey,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
this.crypto = crypto
|
||||
|
||||
this.operators = new OperatorManager(crypto)
|
||||
|
||||
this.rootKeyManager = new RootKeyManager(
|
||||
device,
|
||||
storage,
|
||||
items,
|
||||
mutator,
|
||||
this.operators,
|
||||
identifier,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
internalEventBus.addEventHandler(this, RootKeyManagerEvent.RootKeyManagerKeyStatusChanged)
|
||||
|
||||
this.itemsEncryption = new ItemsEncryptionService(items, payloads, storage, this.operators, keys, internalEventBus)
|
||||
|
||||
UuidGenerator.SetGenerator(this.crypto.generateUUID)
|
||||
}
|
||||
|
||||
@@ -168,25 +148,21 @@ export class EncryptionService
|
||||
}
|
||||
}
|
||||
|
||||
public override async blockDeinit(): Promise<void> {
|
||||
await Promise.all([this.rootKeyManager.blockDeinit(), this.itemsEncryption.blockDeinit()])
|
||||
|
||||
return super.blockDeinit()
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.payloads as unknown) = undefined
|
||||
;(this.device as unknown) = undefined
|
||||
;(this.storage as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this.operators as unknown) = undefined
|
||||
|
||||
this.itemsEncryption.deinit()
|
||||
;(this.itemsEncryption as unknown) = undefined
|
||||
|
||||
this.rootKeyManager.deinit()
|
||||
;(this.rootKeyManager as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this._createNewItemsKeyWithRollback as unknown) = undefined
|
||||
;(this._findDefaultItemsKey as unknown) = undefined
|
||||
;(this._decryptErroredRootPayloads as unknown) = undefined
|
||||
;(this._rootKeyEncryptPayloadWithKeyLookup as unknown) = undefined
|
||||
;(this._rootKeyEncryptPayload as unknown) = undefined
|
||||
;(this._rootKeyDecryptPayload as unknown) = undefined
|
||||
;(this._rootKeyDecryptPayloadWithKeyLookup as unknown) = undefined
|
||||
;(this._createDefaultItemsKey as unknown) = undefined
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
@@ -217,7 +193,7 @@ export class EncryptionService
|
||||
return !!this.getRootKey()?.signingKeyPair
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
public async initialize(): Promise<void> {
|
||||
await this.rootKeyManager.initialize()
|
||||
}
|
||||
|
||||
@@ -250,7 +226,7 @@ export class EncryptionService
|
||||
return this.rootKeyManager.getUserVersion()
|
||||
}
|
||||
|
||||
public async upgradeAvailable() {
|
||||
public async upgradeAvailable(): Promise<boolean> {
|
||||
const accountUpgradeAvailable = this.accountUpgradeAvailable()
|
||||
const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable()
|
||||
return accountUpgradeAvailable || passcodeUpgradeAvailable
|
||||
@@ -268,31 +244,12 @@ export class EncryptionService
|
||||
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* When the key system root key changes, we must re-encrypt all vault items keys
|
||||
* with this new key system root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
|
||||
const keySystemItemsKeys = this.keys.getKeySystemItemsKeys(keySystemIdentifier)
|
||||
if (keySystemItemsKeys.length > 0) {
|
||||
await this.mutator.setItemsDirty(keySystemItemsKeys)
|
||||
}
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
const usecase = new CreateNewItemsKeyWithRollbackUseCase(
|
||||
this.mutator,
|
||||
this.items,
|
||||
this.storage,
|
||||
this.operators,
|
||||
this.rootKeyManager,
|
||||
)
|
||||
return usecase.execute()
|
||||
return this._createNewItemsKeyWithRollback.execute()
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
const usecase = new DecryptErroredRootPayloadsUseCase(this.payloads, this.operators, this.keys, this.rootKeyManager)
|
||||
await usecase.execute()
|
||||
await this._decryptErroredRootPayloads.execute()
|
||||
|
||||
await this.itemsEncryption.decryptErroredItemPayloads()
|
||||
}
|
||||
@@ -326,18 +283,10 @@ export class EncryptionService
|
||||
usesKeySystemRootKeyWithKeyLookup,
|
||||
} = split
|
||||
|
||||
const rootKeyEncryptWithKeyLookupUsecase = new RootKeyEncryptPayloadWithKeyLookupUseCase(
|
||||
this.operators,
|
||||
this.keys,
|
||||
this.rootKeyManager,
|
||||
)
|
||||
|
||||
const rootKeyEncryptUsecase = new RootKeyEncryptPayloadUseCase(this.operators)
|
||||
|
||||
const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined
|
||||
|
||||
if (usesRootKey) {
|
||||
const rootKeyEncrypted = await rootKeyEncryptUsecase.executeMany(
|
||||
const rootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany(
|
||||
usesRootKey.items,
|
||||
usesRootKey.key,
|
||||
signingKeyPair,
|
||||
@@ -346,7 +295,7 @@ export class EncryptionService
|
||||
}
|
||||
|
||||
if (usesRootKeyWithKeyLookup) {
|
||||
const rootKeyEncrypted = await rootKeyEncryptWithKeyLookupUsecase.executeMany(
|
||||
const rootKeyEncrypted = await this._rootKeyEncryptPayloadWithKeyLookup.executeMany(
|
||||
usesRootKeyWithKeyLookup.items,
|
||||
signingKeyPair,
|
||||
)
|
||||
@@ -354,7 +303,7 @@ export class EncryptionService
|
||||
}
|
||||
|
||||
if (usesKeySystemRootKey) {
|
||||
const keySystemRootKeyEncrypted = await rootKeyEncryptUsecase.executeMany(
|
||||
const keySystemRootKeyEncrypted = await this._rootKeyEncryptPayload.executeMany(
|
||||
usesKeySystemRootKey.items,
|
||||
usesKeySystemRootKey.key,
|
||||
signingKeyPair,
|
||||
@@ -363,7 +312,7 @@ export class EncryptionService
|
||||
}
|
||||
|
||||
if (usesKeySystemRootKeyWithKeyLookup) {
|
||||
const keySystemRootKeyEncrypted = await rootKeyEncryptWithKeyLookupUsecase.executeMany(
|
||||
const keySystemRootKeyEncrypted = await this._rootKeyEncryptPayloadWithKeyLookup.executeMany(
|
||||
usesKeySystemRootKeyWithKeyLookup.items,
|
||||
signingKeyPair,
|
||||
)
|
||||
@@ -423,32 +372,26 @@ export class EncryptionService
|
||||
usesKeySystemRootKeyWithKeyLookup,
|
||||
} = split
|
||||
|
||||
const rootKeyDecryptUseCase = new RootKeyDecryptPayloadUseCase(this.operators)
|
||||
|
||||
const rootKeyDecryptWithKeyLookupUsecase = new RootKeyDecryptPayloadWithKeyLookupUseCase(
|
||||
this.operators,
|
||||
this.keys,
|
||||
this.rootKeyManager,
|
||||
)
|
||||
|
||||
if (usesRootKey) {
|
||||
const rootKeyDecrypted = await rootKeyDecryptUseCase.executeMany<C>(usesRootKey.items, usesRootKey.key)
|
||||
const rootKeyDecrypted = await this._rootKeyDecryptPayload.executeMany<C>(usesRootKey.items, usesRootKey.key)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (usesRootKeyWithKeyLookup) {
|
||||
const rootKeyDecrypted = await rootKeyDecryptWithKeyLookupUsecase.executeMany<C>(usesRootKeyWithKeyLookup.items)
|
||||
const rootKeyDecrypted = await this._rootKeyDecryptPayloadWithKeyLookup.executeMany<C>(
|
||||
usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
if (usesKeySystemRootKey) {
|
||||
const keySystemRootKeyDecrypted = await rootKeyDecryptUseCase.executeMany<C>(
|
||||
const keySystemRootKeyDecrypted = await this._rootKeyDecryptPayload.executeMany<C>(
|
||||
usesKeySystemRootKey.items,
|
||||
usesKeySystemRootKey.key,
|
||||
)
|
||||
extendArray(resultParams, keySystemRootKeyDecrypted)
|
||||
}
|
||||
if (usesKeySystemRootKeyWithKeyLookup) {
|
||||
const keySystemRootKeyDecrypted = await rootKeyDecryptWithKeyLookupUsecase.executeMany<C>(
|
||||
const keySystemRootKeyDecrypted = await this._rootKeyDecryptPayloadWithKeyLookup.executeMany<C>(
|
||||
usesKeySystemRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, keySystemRootKeyDecrypted)
|
||||
@@ -640,56 +583,6 @@ export class EncryptionService
|
||||
.createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken)
|
||||
}
|
||||
|
||||
asymmetricallyEncryptMessage(dto: {
|
||||
message: AsymmetricMessagePayload
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
recipientPublicKey: string
|
||||
}): AsymmetricallyEncryptedString {
|
||||
const operator = this.operators.defaultOperator()
|
||||
const encrypted = operator.asymmetricEncrypt({
|
||||
stringToEncrypt: JSON.stringify(dto.message),
|
||||
senderKeyPair: dto.senderKeyPair,
|
||||
senderSigningKeyPair: dto.senderSigningKeyPair,
|
||||
recipientPublicKey: dto.recipientPublicKey,
|
||||
})
|
||||
return encrypted
|
||||
}
|
||||
|
||||
asymmetricallyDecryptMessage<M extends AsymmetricMessagePayload>(dto: {
|
||||
encryptedString: AsymmetricallyEncryptedString
|
||||
trustedSender: TrustedContactInterface | undefined
|
||||
privateKey: string
|
||||
}): M | undefined {
|
||||
const defaultOperator = this.operators.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.encryptedString)
|
||||
const keyOperator = this.operators.operatorForVersion(version)
|
||||
const decryptedResult = keyOperator.asymmetricDecrypt({
|
||||
stringToDecrypt: dto.encryptedString,
|
||||
recipientSecretKey: dto.privateKey,
|
||||
})
|
||||
|
||||
if (!decryptedResult) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!decryptedResult.signatureVerified) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (dto.trustedSender) {
|
||||
if (!dto.trustedSender.isPublicKeyTrusted(decryptedResult.senderPublicKey)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!dto.trustedSender.isSigningKeyTrusted(decryptedResult.signaturePublicKey)) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(decryptedResult.plaintext)
|
||||
}
|
||||
|
||||
asymmetricSignatureVerifyDetached(
|
||||
encryptedString: AsymmetricallyEncryptedString,
|
||||
): AsymmetricSignatureVerificationDetachedResult {
|
||||
@@ -699,7 +592,7 @@ export class EncryptionService
|
||||
return keyOperator.asymmetricSignatureVerifyDetached(encryptedString)
|
||||
}
|
||||
|
||||
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet {
|
||||
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PortablePublicKeySet {
|
||||
const defaultOperator = this.operators.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(string)
|
||||
|
||||
@@ -707,15 +600,6 @@ export class EncryptionService
|
||||
return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string)
|
||||
}
|
||||
|
||||
public async decryptBackupFile(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface<ItemContent>)[]> {
|
||||
const usecase = new DecryptBackupFileUseCase(this)
|
||||
const result = await usecase.execute(file, password)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key params object from a raw object
|
||||
* @param keyParams - The raw key params object to create a KeyParams object from
|
||||
@@ -800,7 +684,7 @@ export class EncryptionService
|
||||
* If so, they must generate the unwrapping key by getting our saved wrapping key keyParams.
|
||||
* After unwrapping, the root key is automatically loaded.
|
||||
*/
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface) {
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface): Promise<void> {
|
||||
return this.rootKeyManager.unwrapRootKey(wrappingKey)
|
||||
}
|
||||
/**
|
||||
@@ -902,7 +786,7 @@ export class EncryptionService
|
||||
* A new root key based items key is needed if our default items key content
|
||||
* isnt equal to our current root key
|
||||
*/
|
||||
const defaultItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
const defaultItemsKey = this._findDefaultItemsKey.execute(this.itemsEncryption.getItemsKeys()).getValue()
|
||||
|
||||
/** Shouldn't be undefined, but if it is, we'll take the corrective action */
|
||||
if (!defaultItemsKey) {
|
||||
@@ -913,8 +797,7 @@ export class EncryptionService
|
||||
}
|
||||
|
||||
public async createNewDefaultItemsKey(): Promise<ItemsKeyInterface> {
|
||||
const usecase = new CreateNewDefaultItemsKeyUseCase(this.mutator, this.items, this.operators, this.rootKeyManager)
|
||||
return usecase.execute()
|
||||
return this._createDefaultItemsKey.execute()
|
||||
}
|
||||
|
||||
public getPasswordCreatedDate(): Date | undefined {
|
||||
@@ -1001,7 +884,7 @@ export class EncryptionService
|
||||
|
||||
private async handleFullSyncCompletion() {
|
||||
/** Always create a new items key after full sync, if no items key is found */
|
||||
const currentItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
const currentItemsKey = this._findDefaultItemsKey.execute(this.itemsEncryption.getItemsKeys()).getValue()
|
||||
if (!currentItemsKey) {
|
||||
await this.createNewDefaultItemsKey()
|
||||
if (this.rootKeyManager.getKeyMode() === KeyMode.WrapperOnly) {
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
ItemsKeyContent,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface, KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { KeyRecoveryStrings, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||
import { ChallengePrompt } from '../Challenge/Prompt/ChallengePrompt'
|
||||
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
||||
import { ChallengeValidation } from '../Challenge/Types/ChallengeValidation'
|
||||
import { EncryptionProviderInterface } from './EncryptionProviderInterface'
|
||||
|
||||
export async function DecryptItemsKeyWithUserFallback(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
ContactPublicKeySet,
|
||||
ContactPublicKeySetInterface,
|
||||
PublicKeyTrustStatus,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { DecryptMessage } from './DecryptMessage'
|
||||
import { OperatorInterface, EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
function createMockPublicKeySetChain(): ContactPublicKeySetInterface {
|
||||
const nMinusOne = new ContactPublicKeySet({
|
||||
encryption: 'encryption-public-key-n-1',
|
||||
signing: 'signing-public-key-n-1',
|
||||
timestamp: new Date(-1),
|
||||
previousKeySet: undefined,
|
||||
})
|
||||
|
||||
const root = new ContactPublicKeySet({
|
||||
encryption: 'encryption-public-key',
|
||||
signing: 'signing-public-key',
|
||||
timestamp: new Date(),
|
||||
previousKeySet: nMinusOne,
|
||||
})
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
describe('DecryptMessage', () => {
|
||||
let usecase: DecryptMessage
|
||||
let operator: jest.Mocked<OperatorInterface>
|
||||
|
||||
beforeEach(() => {
|
||||
operator = {} as jest.Mocked<OperatorInterface>
|
||||
operator.versionForAsymmetricallyEncryptedString = jest.fn().mockReturnValue(ProtocolVersion.V004)
|
||||
|
||||
const operators = {} as jest.Mocked<EncryptionOperatorsInterface>
|
||||
operators.defaultOperator = jest.fn().mockReturnValue(operator)
|
||||
operators.operatorForVersion = jest.fn().mockReturnValue(operator)
|
||||
|
||||
usecase = new DecryptMessage(operators)
|
||||
})
|
||||
|
||||
it('should fail if fails to decrypt', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue(null)
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: undefined,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(true)
|
||||
expect(result.getError()).toEqual('Failed to decrypt message')
|
||||
})
|
||||
|
||||
it('should fail if signature is invalid', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
|
||||
plaintext: 'decrypted',
|
||||
signatureVerified: false,
|
||||
signaturePublicKey: 'signing-public-key',
|
||||
senderPublicKey: 'encryption-public-key',
|
||||
})
|
||||
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: undefined,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(true)
|
||||
expect(result.getError()).toEqual('Failed to verify signature')
|
||||
})
|
||||
|
||||
describe('with trusted sender', () => {
|
||||
it('should fail if encryption public key is not trusted', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
|
||||
plaintext: 'decrypted',
|
||||
signatureVerified: true,
|
||||
signaturePublicKey: 'signing-public-key',
|
||||
senderPublicKey: 'encryption-public-key',
|
||||
})
|
||||
|
||||
const senderContact = {
|
||||
name: 'Other',
|
||||
contactUuid: '456',
|
||||
publicKeySet: createMockPublicKeySetChain(),
|
||||
isMe: false,
|
||||
} as jest.Mocked<TrustedContactInterface>
|
||||
|
||||
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.NotTrusted)
|
||||
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: senderContact,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(true)
|
||||
expect(result.getError()).toEqual('Sender public key is not trusted')
|
||||
})
|
||||
|
||||
it('should fail if signing public key is not trusted', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
|
||||
plaintext: 'decrypted',
|
||||
signatureVerified: true,
|
||||
signaturePublicKey: 'signing-public-key',
|
||||
senderPublicKey: 'encryption-public-key',
|
||||
})
|
||||
|
||||
const senderContact = {
|
||||
name: 'Other',
|
||||
contactUuid: '456',
|
||||
publicKeySet: createMockPublicKeySetChain(),
|
||||
isMe: false,
|
||||
} as jest.Mocked<TrustedContactInterface>
|
||||
|
||||
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
|
||||
senderContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.NotTrusted)
|
||||
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: senderContact,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(true)
|
||||
expect(result.getError()).toEqual('Signature public key is not trusted')
|
||||
})
|
||||
|
||||
it('should succeed with valid signature and encryption key', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
|
||||
plaintext: '{"foo": "bar"}',
|
||||
signatureVerified: true,
|
||||
signaturePublicKey: 'signing-public-key',
|
||||
senderPublicKey: 'encryption-public-key',
|
||||
})
|
||||
|
||||
const senderContact = {
|
||||
name: 'Other',
|
||||
contactUuid: '456',
|
||||
publicKeySet: createMockPublicKeySetChain(),
|
||||
isMe: false,
|
||||
} as jest.Mocked<TrustedContactInterface>
|
||||
|
||||
senderContact.getTrustStatusForPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
|
||||
senderContact.getTrustStatusForSigningPublicKey = jest.fn().mockReturnValue(PublicKeyTrustStatus.Trusted)
|
||||
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: senderContact,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(false)
|
||||
expect(result.getValue()).toEqual({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('without trusted sender', () => {
|
||||
it('should succeed with valid signature and encryption key', () => {
|
||||
operator.asymmetricDecrypt = jest.fn().mockReturnValue({
|
||||
plaintext: '{"foo": "bar"}',
|
||||
signatureVerified: true,
|
||||
signaturePublicKey: 'signing-public-key',
|
||||
senderPublicKey: 'encryption-public-key',
|
||||
})
|
||||
|
||||
const result = usecase.execute({
|
||||
message: 'encrypted',
|
||||
sender: undefined,
|
||||
privateKey: 'private-key',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toEqual(false)
|
||||
expect(result.getValue()).toEqual({ foo: 'bar' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
|
||||
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessagePayload, PublicKeyTrustStatus, TrustedContactInterface } from '@standardnotes/models'
|
||||
|
||||
export class DecryptMessage implements SyncUseCaseInterface<AsymmetricMessagePayload> {
|
||||
constructor(private operators: EncryptionOperatorsInterface) {}
|
||||
|
||||
execute<M extends AsymmetricMessagePayload>(dto: {
|
||||
message: string
|
||||
sender: TrustedContactInterface | undefined
|
||||
privateKey: string
|
||||
}): Result<M> {
|
||||
const defaultOperator = this.operators.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.message)
|
||||
const keyOperator = this.operators.operatorForVersion(version)
|
||||
|
||||
const decryptedResult = keyOperator.asymmetricDecrypt({
|
||||
stringToDecrypt: dto.message,
|
||||
recipientSecretKey: dto.privateKey,
|
||||
})
|
||||
|
||||
if (!decryptedResult) {
|
||||
return Result.fail('Failed to decrypt message')
|
||||
}
|
||||
|
||||
if (!decryptedResult.signatureVerified) {
|
||||
return Result.fail('Failed to verify signature')
|
||||
}
|
||||
|
||||
if (dto.sender) {
|
||||
const publicKeyTrustStatus = dto.sender.getTrustStatusForPublicKey(decryptedResult.senderPublicKey)
|
||||
if (publicKeyTrustStatus !== PublicKeyTrustStatus.Trusted) {
|
||||
return Result.fail('Sender public key is not trusted')
|
||||
}
|
||||
|
||||
const signingKeyTrustStatus = dto.sender.getTrustStatusForSigningPublicKey(decryptedResult.signaturePublicKey)
|
||||
if (signingKeyTrustStatus !== PublicKeyTrustStatus.Trusted) {
|
||||
return Result.fail('Signature public key is not trusted')
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok(JSON.parse(decryptedResult.plaintext))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
|
||||
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessagePayload } from '@standardnotes/models'
|
||||
|
||||
export class DecryptOwnMessage<M extends AsymmetricMessagePayload> implements SyncUseCaseInterface<M> {
|
||||
constructor(private operators: EncryptionOperatorsInterface) {}
|
||||
|
||||
execute(dto: { message: string; privateKey: string; recipientPublicKey: string }): Result<M> {
|
||||
const defaultOperator = this.operators.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.message)
|
||||
const keyOperator = this.operators.operatorForVersion(version)
|
||||
|
||||
const result = keyOperator.asymmetricDecryptOwnMessage({
|
||||
message: dto.message,
|
||||
ownPrivateKey: dto.privateKey,
|
||||
recipientPublicKey: dto.recipientPublicKey,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
const decryptedObject = result.getValue()
|
||||
|
||||
if (!decryptedObject.signatureVerified) {
|
||||
return Result.fail('Failed to verify signature')
|
||||
}
|
||||
|
||||
return Result.ok(JSON.parse(decryptedObject.plaintext))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
|
||||
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessagePayload } from '@standardnotes/models'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export class EncryptMessage implements SyncUseCaseInterface<string> {
|
||||
constructor(private operators: EncryptionOperatorsInterface) {}
|
||||
|
||||
execute(dto: {
|
||||
message: AsymmetricMessagePayload
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
recipientPublicKey: string
|
||||
}): Result<string> {
|
||||
const operator = this.operators.defaultOperator()
|
||||
|
||||
const encrypted = operator.asymmetricEncrypt({
|
||||
stringToEncrypt: JSON.stringify(dto.message),
|
||||
senderKeyPair: dto.keys.encryption,
|
||||
senderSigningKeyPair: dto.keys.signing,
|
||||
recipientPublicKey: dto.recipientPublicKey,
|
||||
})
|
||||
|
||||
return Result.ok(encrypted)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SyncUseCaseInterface, Result } from '@standardnotes/domain-core'
|
||||
import { AsymmetricallyEncryptedString, EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricItemAdditionalData } from '@standardnotes/encryption/src/Domain/Types/EncryptionAdditionalData'
|
||||
|
||||
export class GetMessageAdditionalData implements SyncUseCaseInterface<AsymmetricItemAdditionalData> {
|
||||
constructor(private operators: EncryptionOperatorsInterface) {}
|
||||
|
||||
execute(dto: { message: AsymmetricallyEncryptedString }): Result<AsymmetricItemAdditionalData> {
|
||||
const operator = this.operators.defaultOperator()
|
||||
|
||||
return operator.asymmetricStringGetAdditionalData({ encryptedString: dto.message })
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,10 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { extendArray } from '@standardnotes/utils'
|
||||
import { EncryptionService } from './EncryptionService'
|
||||
import { EncryptionService } from '../EncryptionService'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class DecryptBackupFileUseCase {
|
||||
export class DecryptBackupFile {
|
||||
constructor(private encryption: EncryptionService) {}
|
||||
|
||||
async execute(
|
||||
@@ -53,7 +53,7 @@ export class DecryptBackupFileUseCase {
|
||||
} else if (isDecryptedTransferPayload(item)) {
|
||||
return new DecryptedPayload(item)
|
||||
} else {
|
||||
throw Error('Unhandled case in decryptBackupFile')
|
||||
throw Error('Unhandled case in DecryptBackupFile')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OperatorManager } from '@standardnotes/encryption'
|
||||
import { EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { ProtocolVersionLastNonrootItemsKey, ProtocolVersionLatest, compareVersions } from '@standardnotes/common'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
|
||||
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
|
||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||
import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
/**
|
||||
@@ -20,11 +20,11 @@ import { ContentType } from '@standardnotes/domain-core'
|
||||
* Consumer must call sync. If the protocol version <= 003, only one items key should be created,
|
||||
* and its .itemsKey value should be equal to the root key masterKey value.
|
||||
*/
|
||||
export class CreateNewDefaultItemsKeyUseCase {
|
||||
export class CreateNewDefaultItemsKey {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
) {}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class CreateNewDefaultItemsKeyUseCase {
|
||||
itemTemplate = CreateDecryptedItemFromPayload(payload)
|
||||
} else {
|
||||
/** Create independent items key */
|
||||
itemTemplate = this.operatorManager.operatorForVersion(operatorVersion).createItemsKey()
|
||||
itemTemplate = this.operators.operatorForVersion(operatorVersion).createItemsKey()
|
||||
}
|
||||
|
||||
const itemsKeys = this.items.getDisplayableItemsKeys()
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
import { StorageServiceInterface } from './../../../Storage/StorageServiceInterface'
|
||||
import { ItemsKeyMutator, OperatorManager, findDefaultItemsKey } from '@standardnotes/encryption'
|
||||
import { ItemsKeyMutator } from '@standardnotes/encryption'
|
||||
import { MutatorClientInterface } from '../../../Mutator/MutatorClientInterface'
|
||||
import { ItemManagerInterface } from '../../../Item/ItemManagerInterface'
|
||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||
import { CreateNewDefaultItemsKeyUseCase } from './CreateNewDefaultItemsKey'
|
||||
import { RemoveItemsLocallyUseCase } from '../../../UseCase/RemoveItemsLocally'
|
||||
|
||||
export class CreateNewItemsKeyWithRollbackUseCase {
|
||||
private createDefaultItemsKeyUseCase = new CreateNewDefaultItemsKeyUseCase(
|
||||
this.mutator,
|
||||
this.items,
|
||||
this.operatorManager,
|
||||
this.rootKeyManager,
|
||||
)
|
||||
|
||||
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)
|
||||
import { CreateNewDefaultItemsKey } from './CreateNewDefaultItemsKey'
|
||||
import { RemoveItemsLocally } from '../../../UseCase/RemoveItemsLocally'
|
||||
import { FindDefaultItemsKey } from './FindDefaultItemsKey'
|
||||
|
||||
export class CreateNewItemsKeyWithRollback {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
private createDefaultItemsKey: CreateNewDefaultItemsKey,
|
||||
private removeItemsLocally: RemoveItemsLocally,
|
||||
private findDefaultItemsKey: FindDefaultItemsKey,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<() => Promise<void>> {
|
||||
const currentDefaultItemsKey = findDefaultItemsKey(this.items.getDisplayableItemsKeys())
|
||||
const newDefaultItemsKey = await this.createDefaultItemsKeyUseCase.execute()
|
||||
const currentDefaultItemsKey = this.findDefaultItemsKey.execute(this.items.getDisplayableItemsKeys()).getValue()
|
||||
const newDefaultItemsKey = await this.createDefaultItemsKey.execute()
|
||||
|
||||
const rollback = async () => {
|
||||
await this.removeItemsLocallyUsecase.execute([newDefaultItemsKey])
|
||||
await this.removeItemsLocally.execute([newDefaultItemsKey])
|
||||
|
||||
if (currentDefaultItemsKey) {
|
||||
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ItemsKeyInterface } from '@standardnotes/models'
|
||||
|
||||
export class FindDefaultItemsKey implements SyncUseCaseInterface<ItemsKeyInterface | undefined> {
|
||||
execute(itemsKeys: ItemsKeyInterface[]): Result<ItemsKeyInterface | undefined> {
|
||||
if (itemsKeys.length === 1) {
|
||||
return Result.ok(itemsKeys[0])
|
||||
}
|
||||
|
||||
const defaultKeys = itemsKeys.filter((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
if (defaultKeys.length === 0) {
|
||||
return Result.ok(undefined)
|
||||
}
|
||||
|
||||
if (defaultKeys.length === 1) {
|
||||
return Result.ok(defaultKeys[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Prioritize one that is synced, as neverSynced keys will likely be deleted after
|
||||
* DownloadFirst sync.
|
||||
*/
|
||||
const syncedKeys = defaultKeys.filter((key) => !key.neverSynced)
|
||||
if (syncedKeys.length > 0) {
|
||||
return Result.ok(syncedKeys[0])
|
||||
}
|
||||
|
||||
return Result.ok(undefined)
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
PayloadEmitSource,
|
||||
SureFindPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { PayloadManagerInterface } from './../../../Payloads/PayloadManagerInterface'
|
||||
import { KeySystemKeyManagerInterface, OperatorManager, isErrorDecryptingParameters } from '@standardnotes/encryption'
|
||||
import { RootKeyDecryptPayloadWithKeyLookupUseCase } from './DecryptPayloadWithKeyLookup'
|
||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||
import { PayloadManagerInterface } from '../../../Payloads/PayloadManagerInterface'
|
||||
import { isErrorDecryptingParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import { DecryptTypeAPayloadWithKeyLookup } from './DecryptPayloadWithKeyLookup'
|
||||
import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
|
||||
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class DecryptErroredRootPayloadsUseCase {
|
||||
export class DecryptErroredTypeAPayloads {
|
||||
constructor(
|
||||
private payloads: PayloadManagerInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
private operatorManager: EncryptionOperatorsInterface,
|
||||
private keySystemKeyManager: KeySystemKeyManagerInterface,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
) {}
|
||||
@@ -28,7 +29,7 @@ export class DecryptErroredRootPayloadsUseCase {
|
||||
return
|
||||
}
|
||||
|
||||
const usecase = new RootKeyDecryptPayloadWithKeyLookupUseCase(
|
||||
const usecase = new DecryptTypeAPayloadWithKeyLookup(
|
||||
this.operatorManager,
|
||||
this.keySystemKeyManager,
|
||||
this.rootKeyManager,
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
DecryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
OperatorManager,
|
||||
decryptPayload,
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
EncryptedPayloadInterface,
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export class RootKeyDecryptPayloadUseCase {
|
||||
constructor(private operatorManager: OperatorManager) {}
|
||||
export class DecryptTypeAPayload {
|
||||
constructor(private operatorManager: EncryptionOperatorsInterface) {}
|
||||
|
||||
async executeOne<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
DecryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
OperatorManager,
|
||||
} from '@standardnotes/encryption'
|
||||
import { DecryptedParameters, ErrorDecryptingParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
EncryptedPayloadInterface,
|
||||
@@ -12,12 +7,13 @@ import {
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
import { RootKeyDecryptPayloadUseCase } from './DecryptPayload'
|
||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||
import { DecryptTypeAPayload } from './DecryptPayload'
|
||||
import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
|
||||
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class RootKeyDecryptPayloadWithKeyLookupUseCase {
|
||||
export class DecryptTypeAPayloadWithKeyLookup {
|
||||
constructor(
|
||||
private operatorManager: OperatorManager,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private keySystemKeyManager: KeySystemKeyManagerInterface,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
) {}
|
||||
@@ -43,7 +39,7 @@ export class RootKeyDecryptPayloadWithKeyLookupUseCase {
|
||||
}
|
||||
}
|
||||
|
||||
const usecase = new RootKeyDecryptPayloadUseCase(this.operatorManager)
|
||||
const usecase = new DecryptTypeAPayload(this.operators)
|
||||
|
||||
return usecase.executeOne(payload, key)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { EncryptedOutputParameters, OperatorManager, encryptPayload } from '@standardnotes/encryption'
|
||||
import { EncryptedOutputParameters, EncryptionOperatorsInterface, encryptPayload } from '@standardnotes/encryption'
|
||||
import { DecryptedPayloadInterface, KeySystemRootKeyInterface, RootKeyInterface } from '@standardnotes/models'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export class RootKeyEncryptPayloadUseCase {
|
||||
constructor(private operatorManager: OperatorManager) {}
|
||||
export class EncryptTypeAPayload {
|
||||
constructor(private operators: EncryptionOperatorsInterface) {}
|
||||
|
||||
async executeOne(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: RootKeyInterface | KeySystemRootKeyInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters> {
|
||||
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
|
||||
return encryptPayload(payload, key, this.operators, signingKeyPair)
|
||||
}
|
||||
|
||||
async executeMany(
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EncryptedOutputParameters, KeySystemKeyManagerInterface, OperatorManager } from '@standardnotes/encryption'
|
||||
import { EncryptedOutputParameters, EncryptionOperatorsInterface } from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
DecryptedPayloadInterface,
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
import { RootKeyEncryptPayloadUseCase } from './EncryptPayload'
|
||||
import { RootKeyManager } from '../../RootKey/RootKeyManager'
|
||||
import { EncryptTypeAPayload } from './EncryptPayload'
|
||||
import { RootKeyManager } from '../../../RootKeyManager/RootKeyManager'
|
||||
import { KeySystemKeyManagerInterface } from '../../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class RootKeyEncryptPayloadWithKeyLookupUseCase {
|
||||
export class EncryptTypeAPayloadWithKeyLookup {
|
||||
constructor(
|
||||
private operatorManager: OperatorManager,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private keySystemKeyManager: KeySystemKeyManagerInterface,
|
||||
private rootKeyManager: RootKeyManager,
|
||||
) {}
|
||||
@@ -35,7 +36,7 @@ export class RootKeyEncryptPayloadWithKeyLookupUseCase {
|
||||
throw Error('Attempting root key encryption with no root key')
|
||||
}
|
||||
|
||||
const usecase = new RootKeyEncryptPayloadUseCase(this.operatorManager)
|
||||
const usecase = new EncryptTypeAPayload(this.operators)
|
||||
return usecase.executeOne(payload, key, signingKeyPair)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FeatureStatus } from './FeatureStatus'
|
||||
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
|
||||
|
||||
export interface FeaturesClientInterface {
|
||||
initializeFromDisk(): void
|
||||
getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus
|
||||
hasMinimumRole(role: string): boolean
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
||||
import { LegacyApiServiceInterface } from './../Api/LegacyApiServiceInterface'
|
||||
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
||||
import { FileItem } from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { ChallengeServiceInterface } from '../Challenge'
|
||||
import { InternalEventBusInterface, MutatorClientInterface } from '..'
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
|
||||
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { FileService } from './FileService'
|
||||
import { BackupServiceInterface } from '@standardnotes/files'
|
||||
import { HttpServiceInterface } from '@standardnotes/api'
|
||||
|
||||
describe('fileService', () => {
|
||||
let apiService: ApiServiceInterface
|
||||
let apiService: LegacyApiServiceInterface
|
||||
let itemManager: ItemManagerInterface
|
||||
let mutator: MutatorClientInterface
|
||||
let syncService: SyncServiceInterface
|
||||
@@ -26,7 +27,7 @@ describe('fileService', () => {
|
||||
let http: HttpServiceInterface
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<ApiServiceInterface>
|
||||
apiService = {} as jest.Mocked<LegacyApiServiceInterface>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
apiService.deleteFile = jest.fn().mockReturnValue({})
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
|
||||
import { EncryptionProviderInterface, SNItemsKey } from '@standardnotes/encryption'
|
||||
import { SNItemsKey } from '@standardnotes/encryption'
|
||||
import {
|
||||
DownloadAndDecryptFileOperation,
|
||||
EncryptAndUploadFileOperation,
|
||||
@@ -50,6 +50,7 @@ import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
|
||||
import { log, LoggingDomain } from '../Logging'
|
||||
import { SharedVaultServer, SharedVaultServerInterface, HttpServiceInterface } from '@standardnotes/api'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
const OneHundredMb = 100 * 1_000_000
|
||||
|
||||
@@ -60,7 +61,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
constructor(
|
||||
private api: FilesApiInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private syncService: SyncServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryptor: EncryptionProviderInterface,
|
||||
private challengor: ChallengeServiceInterface,
|
||||
http: HttpServiceInterface,
|
||||
@@ -80,7 +81,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
;(this.encryptedCache as unknown) = undefined
|
||||
;(this.api as unknown) = undefined
|
||||
;(this.encryptor as unknown) = undefined
|
||||
;(this.syncService as unknown) = undefined
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.alertService as unknown) = undefined
|
||||
;(this.challengor as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
@@ -270,7 +271,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
operation.vault,
|
||||
)
|
||||
|
||||
await this.syncService.sync()
|
||||
await this.sync.sync()
|
||||
|
||||
return file
|
||||
}
|
||||
@@ -401,7 +402,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
}
|
||||
|
||||
await this.mutator.setItemToBeDeleted(file)
|
||||
await this.syncService.sync()
|
||||
await this.sync.sync()
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HistoryMap } from '@standardnotes/models'
|
||||
import { HistoryEntry, HistoryMap, SNNote } from '@standardnotes/models'
|
||||
|
||||
export interface HistoryServiceInterface {
|
||||
getHistoryMapCopy(): HistoryMap
|
||||
sessionHistoryForItem(item: SNNote): HistoryEntry[]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { InternalEventInterface } from './../Internal/InternalEventInterface'
|
||||
import { ApplicationStageChangedEventPayload } from './../Event/ApplicationStageChangedEventPayload'
|
||||
import { ApplicationEvent } from './../Event/ApplicationEvent'
|
||||
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
@@ -10,7 +14,10 @@ import { HomeServerServiceInterface } from './HomeServerServiceInterface'
|
||||
import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfiguration'
|
||||
import { HomeServerStatus } from './HomeServerStatus'
|
||||
|
||||
export class HomeServerService extends AbstractService implements HomeServerServiceInterface {
|
||||
export class HomeServerService
|
||||
extends AbstractService
|
||||
implements HomeServerServiceInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private readonly HOME_SERVER_DATA_DIRECTORY_NAME = '.homeserver'
|
||||
|
||||
constructor(
|
||||
@@ -20,26 +27,28 @@ export class HomeServerService extends AbstractService implements HomeServerServ
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
|
||||
switch (stage) {
|
||||
case ApplicationStage.StorageDecrypted_09: {
|
||||
await this.setHomeServerDataLocationOnDevice()
|
||||
break
|
||||
}
|
||||
case ApplicationStage.Launched_10: {
|
||||
await this.startHomeServerIfItIsEnabled()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override deinit() {
|
||||
;(this.desktopDevice as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
override async handleApplicationStage(stage: ApplicationStage) {
|
||||
await super.handleApplicationStage(stage)
|
||||
|
||||
switch (stage) {
|
||||
case ApplicationStage.StorageDecrypted_09: {
|
||||
await this.setHomeServerDataLocationOnDevice()
|
||||
break
|
||||
}
|
||||
case ApplicationStage.Launched_10: {
|
||||
await this.startHomeServerIfItIsEnabled()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getHomeServerStatus(): Promise<HomeServerStatus> {
|
||||
const isHomeServerRunning = await this.desktopDevice.isHomeServerRunning()
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { FindDefaultItemsKey } from './../Encryption/UseCase/ItemsKey/FindDefaultItemsKey'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
isErrorDecryptingParameters,
|
||||
OperatorManager,
|
||||
StandardException,
|
||||
encryptPayload,
|
||||
decryptPayload,
|
||||
EncryptedOutputParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
@@ -33,22 +32,24 @@ import { AbstractService } from '../Service/AbstractService'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class ItemsEncryptionService extends AbstractService {
|
||||
private removeItemsObserver!: () => void
|
||||
public userVersion?: ProtocolVersion
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
private items: ItemManagerInterface,
|
||||
private payloads: PayloadManagerInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private keys: KeySystemKeyManagerInterface,
|
||||
private _findDefaultItemsKey: FindDefaultItemsKey,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeItemsObserver = this.itemManager.addObserver([ContentType.TYPES.ItemsKey], ({ changed, inserted }) => {
|
||||
this.removeItemsObserver = this.items.addObserver([ContentType.TYPES.ItemsKey], ({ changed, inserted }) => {
|
||||
if (changed.concat(inserted).length > 0) {
|
||||
void this.decryptErroredItemPayloads()
|
||||
}
|
||||
@@ -56,10 +57,10 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.operatorManager as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.payloads as unknown) = undefined
|
||||
;(this.storage as unknown) = undefined
|
||||
;(this.operators as unknown) = undefined
|
||||
;(this.keys as unknown) = undefined
|
||||
this.removeItemsObserver()
|
||||
;(this.removeItemsObserver as unknown) = undefined
|
||||
@@ -72,22 +73,20 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
* disk using latest encryption status.
|
||||
*/
|
||||
async repersistAllItems(): Promise<void> {
|
||||
const items = this.itemManager.items
|
||||
const items = this.items.items
|
||||
const payloads = items.map((item) => item.payload)
|
||||
return this.storageService.savePayloads(payloads)
|
||||
return this.storage.savePayloads(payloads)
|
||||
}
|
||||
|
||||
public getItemsKeys(): ItemsKeyInterface[] {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
return this.items.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public itemsKeyForEncryptedPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
const keySystemItemsKeys = this.itemManager.getItems<KeySystemItemsKeyInterface>(
|
||||
ContentType.TYPES.KeySystemItemsKey,
|
||||
)
|
||||
const keySystemItemsKeys = this.items.getItems<KeySystemItemsKeyInterface>(ContentType.TYPES.KeySystemItemsKey)
|
||||
|
||||
return [...itemsKeys, ...keySystemItemsKeys].find(
|
||||
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
|
||||
@@ -95,7 +94,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
}
|
||||
|
||||
public getDefaultItemsKey(): ItemsKeyInterface | undefined {
|
||||
return findDefaultItemsKey(this.getItemsKeys())
|
||||
return this._findDefaultItemsKey.execute(this.getItemsKeys()).getValue()
|
||||
}
|
||||
|
||||
private keyToUseForItemEncryption(
|
||||
@@ -173,7 +172,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
throw Error('Attempting to encrypt payload with no UuidGenerator.')
|
||||
}
|
||||
|
||||
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
|
||||
return encryptPayload(payload, key, this.operators, signingKeyPair)
|
||||
}
|
||||
|
||||
public async encryptPayloads(
|
||||
@@ -218,7 +217,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
}
|
||||
}
|
||||
|
||||
return decryptPayload(payload, key, this.operatorManager)
|
||||
return decryptPayload(payload, key, this.operators)
|
||||
}
|
||||
|
||||
public async decryptPayloadsWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
@@ -235,7 +234,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
}
|
||||
|
||||
public async decryptErroredItemPayloads(): Promise<void> {
|
||||
const erroredItemPayloads = this.payloadManager.invalidPayloads.filter(
|
||||
const erroredItemPayloads = this.payloads.invalidPayloads.filter(
|
||||
(i) =>
|
||||
!ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
|
||||
)
|
||||
@@ -260,7 +259,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
}
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged)
|
||||
await this.payloads.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
|
||||
@@ -16,13 +17,19 @@ import {
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../Item/ItemManagerInterface'
|
||||
import { KeySystemKeyManagerInterface } from '@standardnotes/encryption'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { ApplicationEvent } from '../Event/ApplicationEvent'
|
||||
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
|
||||
import { KeySystemKeyManagerInterface } from './KeySystemKeyManagerInterface'
|
||||
|
||||
const RootKeyStorageKeyPrefix = 'key-system-root-key-'
|
||||
|
||||
export class KeySystemKeyManager extends AbstractService implements KeySystemKeyManagerInterface {
|
||||
export class KeySystemKeyManager
|
||||
extends AbstractService
|
||||
implements KeySystemKeyManagerInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private rootKeyMemoryCache: Record<KeySystemIdentifier, KeySystemRootKeyInterface> = {}
|
||||
|
||||
constructor(
|
||||
@@ -34,9 +41,12 @@ export class KeySystemKeyManager extends AbstractService implements KeySystemKey
|
||||
super(eventBus)
|
||||
}
|
||||
|
||||
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
if (stage === ApplicationStage.StorageDecrypted_09) {
|
||||
this.loadRootKeysFromStorage()
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.StorageDecrypted_09) {
|
||||
this.loadRootKeysFromStorage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +69,17 @@ export class KeySystemKeyManager extends AbstractService implements KeySystemKey
|
||||
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
||||
}
|
||||
|
||||
/**
|
||||
* When the key system root key changes, we must re-encrypt all vault items keys
|
||||
* with this new key system root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
|
||||
const keySystemItemsKeys = this.getKeySystemItemsKeys(keySystemIdentifier)
|
||||
if (keySystemItemsKeys.length > 0) {
|
||||
await this.mutator.setItemsDirty(keySystemItemsKeys)
|
||||
}
|
||||
}
|
||||
|
||||
public intakeNonPersistentKeySystemRootKey(
|
||||
key: KeySystemRootKeyInterface,
|
||||
storage: KeySystemRootKeyStorageMode,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
EncryptedItemInterface,
|
||||
KeySystemIdentifier,
|
||||
KeySystemItemsKeyInterface,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export interface KeySystemKeyManagerInterface {
|
||||
getAllKeySystemItemsKeys(): (KeySystemItemsKeyInterface | EncryptedItemInterface)[]
|
||||
getKeySystemItemsKeys(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface[]
|
||||
getPrimaryKeySystemItemsKey(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface
|
||||
|
||||
/** Returns synced root keys, in addition to any local or ephemeral keys */
|
||||
getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[]
|
||||
getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[]
|
||||
getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[]
|
||||
getKeySystemRootKeyWithToken(
|
||||
systemIdentifier: KeySystemIdentifier,
|
||||
keyIdentifier: string,
|
||||
): KeySystemRootKeyInterface | undefined
|
||||
getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined
|
||||
reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void>
|
||||
|
||||
intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void
|
||||
undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void
|
||||
|
||||
clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void
|
||||
deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise<void>
|
||||
deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise<void>
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DecryptBackupFile } from '../Encryption/UseCase/DecryptBackupFile'
|
||||
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
@@ -18,9 +19,9 @@ import {
|
||||
isEncryptedTransferPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
const Strings = {
|
||||
UnsupportedBackupFileVersion:
|
||||
@@ -42,12 +43,13 @@ export type ImportDataReturnType =
|
||||
export class ImportDataUseCase {
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private syncService: SyncServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private protectionService: ProtectionsClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private challengeService: ChallengeServiceInterface,
|
||||
private historyService: HistoryServiceInterface,
|
||||
private _decryptBackFile: DecryptBackupFile,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -107,7 +109,7 @@ export class ImportDataUseCase {
|
||||
}
|
||||
})
|
||||
|
||||
const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password)
|
||||
const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
@@ -131,7 +133,7 @@ export class ImportDataUseCase {
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
)
|
||||
|
||||
const promise = this.syncService.sync()
|
||||
const promise = this.sync.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
|
||||
import { ChallengeReason } from '../Challenge'
|
||||
import { ChallengeInterface, ChallengeReason } from '../Challenge'
|
||||
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
||||
import { TimingDisplayOption } from './TimingDisplayOption'
|
||||
|
||||
export interface ProtectionsClientInterface {
|
||||
createLaunchChallenge(): ChallengeInterface | undefined
|
||||
authorizeProtectedActionForItems<T extends DecryptedItem>(files: T[], challengeReason: ChallengeReason): Promise<T[]>
|
||||
authorizeItemAccess(item: DecryptedItem): Promise<boolean>
|
||||
getMobileBiometricsTiming(): MobileUnlockTiming | undefined
|
||||
@@ -17,6 +18,7 @@ export interface ProtectionsClientInterface {
|
||||
hasBiometricsEnabled(): boolean
|
||||
enableBiometrics(): boolean
|
||||
disableBiometrics(): Promise<boolean>
|
||||
|
||||
authorizeAction(
|
||||
reason: ChallengeReason,
|
||||
dto: { fallBackToAccountPassword: boolean; requireAccountPassword: boolean; forcePrompt: boolean },
|
||||
@@ -25,6 +27,12 @@ export interface ProtectionsClientInterface {
|
||||
authorizeRemovingPasscode(): Promise<boolean>
|
||||
authorizeChangingPasscode(): Promise<boolean>
|
||||
authorizeFileImport(): Promise<boolean>
|
||||
authorizeSessionRevoking(): Promise<boolean>
|
||||
authorizeAutolockIntervalChange(): Promise<boolean>
|
||||
authorizeSearchingProtectedNotesText(): Promise<boolean>
|
||||
authorizeBackupCreation(): Promise<boolean>
|
||||
authorizeMfaDisable(): Promise<boolean>
|
||||
|
||||
protectItems<I extends DecryptedItemInterface>(items: I[]): Promise<I[]>
|
||||
unprotectItems<I extends DecryptedItemInterface>(items: I[], reason: ChallengeReason): Promise<I[] | undefined>
|
||||
protectNote(note: SNNote): Promise<SNNote>
|
||||
@@ -33,4 +41,9 @@ export interface ProtectionsClientInterface {
|
||||
unprotectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
protectFile(file: FileItem): Promise<FileItem>
|
||||
unprotectFile(file: FileItem): Promise<FileItem | undefined>
|
||||
|
||||
hasProtectionSources(): boolean
|
||||
hasUnprotectedAccessSession(): boolean
|
||||
getSessionExpiryDate(): Date
|
||||
clearSession(): Promise<void>
|
||||
}
|
||||
|
||||
10
packages/services/src/Domain/RootKeyManager/KeyMode.ts
Normal file
10
packages/services/src/Domain/RootKeyManager/KeyMode.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum KeyMode {
|
||||
/** i.e No account and no passcode */
|
||||
RootKeyNone = 0,
|
||||
/** i.e Account but no passcode */
|
||||
RootKeyOnly = 1,
|
||||
/** i.e Account plus passcode */
|
||||
RootKeyPlusWrapper = 2,
|
||||
/** i.e No account, but passcode */
|
||||
WrapperOnly = 3,
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import {
|
||||
ProtocolVersionLatest,
|
||||
} from '@standardnotes/common'
|
||||
import {
|
||||
KeyMode,
|
||||
CreateNewRootKey,
|
||||
CreateAnyKeyParams,
|
||||
SNRootKey,
|
||||
isErrorDecryptingParameters,
|
||||
OperatorManager,
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypesUsingRootKeyEncryption,
|
||||
@@ -25,19 +24,20 @@ import {
|
||||
RootKeyInterface,
|
||||
RootKeyParamsInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { DeviceInterface } from '../../Device/DeviceInterface'
|
||||
import { InternalEventBusInterface } from '../../Internal/InternalEventBusInterface'
|
||||
import { StorageKey } from '../../Storage/StorageKeys'
|
||||
import { StorageServiceInterface } from '../../Storage/StorageServiceInterface'
|
||||
import { StorageValueModes } from '../../Storage/StorageTypes'
|
||||
import { RootKeyEncryptPayloadUseCase } from '../UseCase/RootEncryption/EncryptPayload'
|
||||
import { RootKeyDecryptPayloadUseCase } from '../UseCase/RootEncryption/DecryptPayload'
|
||||
import { AbstractService } from '../../Service/AbstractService'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
import { DeviceInterface } from '../Device/DeviceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { StorageKey } from '../Storage/StorageKeys'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
|
||||
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
|
||||
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
|
||||
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
|
||||
import { KeyMode } from './KeyMode'
|
||||
|
||||
export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
@@ -49,7 +49,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private operators: OperatorManager,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
@@ -249,7 +249,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
|
||||
const payload = new DecryptedPayload(value)
|
||||
|
||||
const usecase = new RootKeyEncryptPayloadUseCase(this.operators)
|
||||
const usecase = new EncryptTypeAPayload(this.operators)
|
||||
const wrappedKey = await usecase.executeOne(payload, wrappingKey)
|
||||
const wrappedKeyPayload = new EncryptedPayload({
|
||||
...payload.ejected(),
|
||||
@@ -273,7 +273,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
|
||||
const wrappedKey = this.getWrappedRootKey()
|
||||
const payload = new EncryptedPayload(wrappedKey)
|
||||
const usecase = new RootKeyDecryptPayloadUseCase(this.operators)
|
||||
const usecase = new DecryptTypeAPayload(this.operators)
|
||||
const decrypted = await usecase.executeOne<RootKeyContent>(payload, wrappingKey)
|
||||
|
||||
if (isErrorDecryptingParameters(decrypted)) {
|
||||
@@ -433,7 +433,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
* by attempting to decrypt account keys.
|
||||
*/
|
||||
const wrappedKeyPayload = new EncryptedPayload(wrappedRootKey)
|
||||
const usecase = new RootKeyDecryptPayloadUseCase(this.operators)
|
||||
const usecase = new DecryptTypeAPayload(this.operators)
|
||||
const decrypted = await usecase.executeOne(wrappedKeyPayload, wrappingKey)
|
||||
return !isErrorDecryptingParameters(decrypted)
|
||||
} else {
|
||||
@@ -4,7 +4,6 @@ import { log, removeFromArray } from '@standardnotes/utils'
|
||||
import { EventObserver } from '../Event/EventObserver'
|
||||
import { ApplicationServiceInterface } from './ApplicationServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
import { InternalEventPublishStrategy } from '../Internal/InternalEventPublishStrategy'
|
||||
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
|
||||
|
||||
@@ -93,19 +92,14 @@ export abstract class AbstractService<EventName = string, EventData = unknown>
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Application instances will call this function directly when they arrive
|
||||
* at a certain migratory state.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async handleApplicationStage(_stage: ApplicationStage): Promise<void> {
|
||||
// optional override
|
||||
}
|
||||
|
||||
getServiceName(): string {
|
||||
return this.constructor.name
|
||||
}
|
||||
|
||||
isApplicationService(): true {
|
||||
return true
|
||||
}
|
||||
|
||||
log(..._args: unknown[]): void {
|
||||
if (this.loggingEnabled) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
import { ServiceDiagnostics } from '../Diagnostics/ServiceDiagnostics'
|
||||
import { EventObserver } from '../Event/EventObserver'
|
||||
|
||||
@@ -7,6 +6,5 @@ export interface ApplicationServiceInterface<E, D> extends ServiceDiagnostics {
|
||||
addEventObserver(observer: EventObserver<E, D>): () => void
|
||||
blockDeinit(): Promise<void>
|
||||
deinit(): void
|
||||
handleApplicationStage(stage: ApplicationStage): Promise<void>
|
||||
log(message: string, ...args: unknown[]): void
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import { UserRegistrationResponseBody } from '@standardnotes/api'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { SNRootKey } from '@standardnotes/encryption'
|
||||
import { RootKeyInterface } from '@standardnotes/models'
|
||||
import { SessionBody, SignInResponse, User, HttpResponse } from '@standardnotes/responses'
|
||||
import {
|
||||
SessionBody,
|
||||
SignInResponse,
|
||||
User,
|
||||
HttpResponse,
|
||||
SessionListEntry,
|
||||
SessionListResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
|
||||
import { SessionManagerResponse } from './SessionManagerResponse'
|
||||
@@ -11,12 +18,18 @@ export interface SessionsClientInterface {
|
||||
getWorkspaceDisplayIdentifier(): string
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
|
||||
initializeFromDisk(): Promise<void>
|
||||
|
||||
getUser(): User | undefined
|
||||
isSignedIn(): boolean
|
||||
get userUuid(): string
|
||||
getSureUser(): User
|
||||
isSignedIntoFirstPartyServer(): boolean
|
||||
|
||||
getSessionsList(): Promise<HttpResponse<SessionListEntry[]>>
|
||||
revokeSession(sessionId: string): Promise<HttpResponse<SessionListResponse>>
|
||||
revokeAllOtherSessions(): Promise<void>
|
||||
|
||||
isCurrentSessionReadOnly(): boolean | undefined
|
||||
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
signIn(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export type UserKeyPairChangedEventData = {
|
||||
oldKeyPair: PkcKeyPair | undefined
|
||||
oldSigningKeyPair: PkcKeyPair | undefined
|
||||
|
||||
newKeyPair: PkcKeyPair
|
||||
newSigningKeyPair: PkcKeyPair
|
||||
current: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
previous?: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
||||
import { GetVaultUsers } from './UseCase/GetVaultUsers'
|
||||
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
|
||||
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
|
||||
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
|
||||
import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
|
||||
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
|
||||
import { LeaveVault } from './UseCase/LeaveSharedVault'
|
||||
import { InviteToVault } from './UseCase/InviteToVault'
|
||||
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
|
||||
import { GetVaultContacts } from './UseCase/GetVaultContacts'
|
||||
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
|
||||
import { FindContact } from './../Contacts/UseCase/FindContact'
|
||||
import { GetUntrustedPayload } from './../AsymmetricMessage/UseCase/GetUntrustedPayload'
|
||||
import { GetTrustedPayload } from './../AsymmetricMessage/UseCase/GetTrustedPayload'
|
||||
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
|
||||
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
|
||||
import { HandleKeyPairChange } from './../Contacts/UseCase/HandleKeyPairChange'
|
||||
import { CreateSharedVault } from './UseCase/CreateSharedVault'
|
||||
import { GetVault } from './../Vaults/UseCase/GetVault'
|
||||
import { SharedVaultInvitesServer } from '@standardnotes/api'
|
||||
import { SharedVaultService } from './SharedVaultService'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
|
||||
import { InternalEventBusInterface } from '../..'
|
||||
import { ContactPublicKeySetInterface, TrustedContactInterface } from '@standardnotes/models'
|
||||
|
||||
describe('SharedVaultService', () => {
|
||||
let service: SharedVaultService
|
||||
|
||||
beforeEach(() => {
|
||||
const sync = {} as jest.Mocked<SyncServiceInterface>
|
||||
sync.addEventObserver = jest.fn()
|
||||
|
||||
const items = {} as jest.Mocked<ItemManagerInterface>
|
||||
items.addObserver = jest.fn()
|
||||
|
||||
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
|
||||
const session = {} as jest.Mocked<SessionsClientInterface>
|
||||
const vaults = {} as jest.Mocked<VaultServiceInterface>
|
||||
const invitesServer = {} as jest.Mocked<SharedVaultInvitesServer>
|
||||
const getVault = {} as jest.Mocked<GetVault>
|
||||
const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault>
|
||||
const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange>
|
||||
const notifyVaultUsersOfKeyRotation = {} as jest.Mocked<NotifyVaultUsersOfKeyRotation>
|
||||
const sendVaultDataChangeMessage = {} as jest.Mocked<SendVaultDataChangedMessage>
|
||||
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
|
||||
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
|
||||
const findContact = {} as jest.Mocked<FindContact>
|
||||
const getAllContacts = {} as jest.Mocked<GetAllContacts>
|
||||
const getVaultContacts = {} as jest.Mocked<GetVaultContacts>
|
||||
const acceptVaultInvite = {} as jest.Mocked<AcceptVaultInvite>
|
||||
const inviteToVault = {} as jest.Mocked<InviteToVault>
|
||||
const leaveVault = {} as jest.Mocked<LeaveVault>
|
||||
const deleteThirdPartyVault = {} as jest.Mocked<DeleteThirdPartyVault>
|
||||
const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault>
|
||||
const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault>
|
||||
const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
|
||||
const removeVaultMember = {} as jest.Mocked<RemoveVaultMember>
|
||||
const getSharedVaultUsersUseCase = {} as jest.Mocked<GetVaultUsers>
|
||||
|
||||
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
eventBus.addEventHandler = jest.fn()
|
||||
|
||||
service = new SharedVaultService(
|
||||
sync,
|
||||
items,
|
||||
encryption,
|
||||
session,
|
||||
vaults,
|
||||
invitesServer,
|
||||
getVault,
|
||||
createSharedVaultUseCase,
|
||||
handleKeyPairChange,
|
||||
notifyVaultUsersOfKeyRotation,
|
||||
sendVaultDataChangeMessage,
|
||||
getTrustedPayload,
|
||||
getUntrustedPayload,
|
||||
findContact,
|
||||
getAllContacts,
|
||||
getVaultContacts,
|
||||
acceptVaultInvite,
|
||||
inviteToVault,
|
||||
leaveVault,
|
||||
deleteThirdPartyVault,
|
||||
shareContactWithVault,
|
||||
convertToSharedVault,
|
||||
deleteSharedVaultUseCase,
|
||||
removeVaultMember,
|
||||
getSharedVaultUsersUseCase,
|
||||
eventBus,
|
||||
)
|
||||
})
|
||||
|
||||
describe('shareContactWithVaults', () => {
|
||||
it('should throw if attempting to share self contact', async () => {
|
||||
const contact = {
|
||||
name: 'Other',
|
||||
contactUuid: '456',
|
||||
publicKeySet: {} as ContactPublicKeySetInterface,
|
||||
isMe: true,
|
||||
} as jest.Mocked<TrustedContactInterface>
|
||||
|
||||
await expect(service.shareContactWithVaults(contact)).rejects.toThrow('Cannot share self contact')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||
import { InviteContactToSharedVaultUseCase } from './UseCase/InviteContactToSharedVault'
|
||||
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
|
||||
import { InviteToVault } from './UseCase/InviteToVault'
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
@@ -10,17 +9,7 @@ import {
|
||||
SharedVaultPermission,
|
||||
UserEventType,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
HttpServiceInterface,
|
||||
SharedVaultServerInterface,
|
||||
SharedVaultUsersServerInterface,
|
||||
SharedVaultInvitesServerInterface,
|
||||
SharedVaultUsersServer,
|
||||
SharedVaultInvitesServer,
|
||||
SharedVaultServer,
|
||||
AsymmetricMessageServerInterface,
|
||||
AsymmetricMessageServer,
|
||||
} from '@standardnotes/api'
|
||||
import { SharedVaultInvitesServer } from '@standardnotes/api'
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
PayloadEmitSource,
|
||||
@@ -32,61 +21,72 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { SharedVaultServiceInterface } from './SharedVaultServiceInterface'
|
||||
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { GetSharedVaultUsersUseCase } from './UseCase/GetSharedVaultUsers'
|
||||
import { RemoveVaultMemberUseCase } from './UseCase/RemoveSharedVaultMember'
|
||||
import { GetVaultUsers } from './UseCase/GetVaultUsers'
|
||||
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||
import { ContactServiceInterface } from '../Contacts/ContactServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from '../Event/SyncEvent'
|
||||
import { SessionEvent } from '../Session/SessionEvent'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { LeaveVaultUseCase } from './UseCase/LeaveSharedVault'
|
||||
import { LeaveVault } from './UseCase/LeaveSharedVault'
|
||||
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
|
||||
import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent'
|
||||
import { DeleteExternalSharedVaultUseCase } from './UseCase/DeleteExternalSharedVault'
|
||||
import { DeleteSharedVaultUseCase } from './UseCase/DeleteSharedVault'
|
||||
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
|
||||
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
|
||||
import { VaultServiceEvent, VaultServiceEventPayload } from '../Vaults/VaultServiceEvent'
|
||||
import { AcceptTrustedSharedVaultInvite } from './UseCase/AcceptTrustedSharedVaultInvite'
|
||||
import { GetAsymmetricMessageTrustedPayload } from '../AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload'
|
||||
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
|
||||
import { GetTrustedPayload } from '../AsymmetricMessage/UseCase/GetTrustedPayload'
|
||||
import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord'
|
||||
import { GetAsymmetricMessageUntrustedPayload } from '../AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload'
|
||||
import { ShareContactWithAllMembersOfSharedVaultUseCase } from './UseCase/ShareContactWithAllMembersOfSharedVault'
|
||||
import { GetSharedVaultTrustedContacts } from './UseCase/GetSharedVaultTrustedContacts'
|
||||
import { NotifySharedVaultUsersOfRootKeyRotationUseCase } from './UseCase/NotifySharedVaultUsersOfRootKeyRotation'
|
||||
import { CreateSharedVaultUseCase } from './UseCase/CreateSharedVault'
|
||||
import { SendSharedVaultMetadataChangedMessageToAll } from './UseCase/SendSharedVaultMetadataChangedMessageToAll'
|
||||
import { ConvertToSharedVaultUseCase } from './UseCase/ConvertToSharedVault'
|
||||
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { GetUntrustedPayload } from '../AsymmetricMessage/UseCase/GetUntrustedPayload'
|
||||
import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
|
||||
import { GetVaultContacts } from './UseCase/GetVaultContacts'
|
||||
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
|
||||
import { CreateSharedVault } from './UseCase/CreateSharedVault'
|
||||
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
|
||||
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
|
||||
import { GetVault } from '../Vaults/UseCase/GetVault'
|
||||
import { ContentType, Result } from '@standardnotes/domain-core'
|
||||
import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange'
|
||||
import { FindContact } from '../Contacts/UseCase/FindContact'
|
||||
import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
export class SharedVaultService
|
||||
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload>
|
||||
implements SharedVaultServiceInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private server: SharedVaultServerInterface
|
||||
private usersServer: SharedVaultUsersServerInterface
|
||||
private invitesServer: SharedVaultInvitesServerInterface
|
||||
private messageServer: AsymmetricMessageServerInterface
|
||||
|
||||
private pendingInvites: Record<string, PendingSharedVaultInviteRecord> = {}
|
||||
|
||||
constructor(
|
||||
http: HttpServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private session: SessionsClientInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
private vaults: VaultServiceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private invitesServer: SharedVaultInvitesServer,
|
||||
private getVault: GetVault,
|
||||
private createSharedVaultUseCase: CreateSharedVault,
|
||||
private handleKeyPairChange: HandleKeyPairChange,
|
||||
private notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
|
||||
private sendVaultDataChangeMessage: SendVaultDataChangedMessage,
|
||||
private getTrustedPayload: GetTrustedPayload,
|
||||
private getUntrustedPayload: GetUntrustedPayload,
|
||||
private findContact: FindContact,
|
||||
private getAllContacts: GetAllContacts,
|
||||
private getVaultContacts: GetVaultContacts,
|
||||
private acceptVaultInvite: AcceptVaultInvite,
|
||||
private inviteToVault: InviteToVault,
|
||||
private leaveVault: LeaveVault,
|
||||
private deleteThirdPartyVault: DeleteThirdPartyVault,
|
||||
private shareContactWithVault: ShareContactWithVault,
|
||||
private convertToSharedVault: ConvertToSharedVault,
|
||||
private deleteSharedVaultUseCase: DeleteSharedVault,
|
||||
private removeVaultMember: RemoveVaultMember,
|
||||
private getSharedVaultUsersUseCase: GetVaultUsers,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
@@ -95,21 +95,6 @@ export class SharedVaultService
|
||||
eventBus.addEventHandler(this, UserEventServiceEvent.UserEventReceived)
|
||||
eventBus.addEventHandler(this, VaultServiceEvent.VaultRootKeyRotated)
|
||||
|
||||
this.server = new SharedVaultServer(http)
|
||||
this.usersServer = new SharedVaultUsersServer(http)
|
||||
this.invitesServer = new SharedVaultInvitesServer(http)
|
||||
this.messageServer = new AsymmetricMessageServer(http)
|
||||
|
||||
this.eventDisposers.push(
|
||||
sync.addEventObserver(async (event, data) => {
|
||||
if (event === SyncEvent.ReceivedSharedVaultInvites) {
|
||||
void this.processInboundInvites(data as SyncEventReceivedSharedVaultInvitesData)
|
||||
} else if (event === SyncEvent.ReceivedRemoteSharedVaults) {
|
||||
void this.notifyCollaborationStatusChanged()
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
this.eventDisposers.push(
|
||||
items.addObserver<TrustedContactInterface>(
|
||||
ContentType.TYPES.TrustedContact,
|
||||
@@ -138,26 +123,32 @@ export class SharedVaultService
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SessionEvent.UserKeyPairChanged) {
|
||||
void this.invitesServer.deleteAllInboundInvites()
|
||||
|
||||
const eventData = event.payload as UserKeyPairChangedEventData
|
||||
|
||||
void this.handleKeyPairChange.execute({
|
||||
newKeys: eventData.current,
|
||||
previousKeys: eventData.previous,
|
||||
})
|
||||
} else if (event.type === UserEventServiceEvent.UserEventReceived) {
|
||||
await this.handleUserEvent(event.payload as UserEventServiceEventPayload)
|
||||
} else if (event.type === VaultServiceEvent.VaultRootKeyRotated) {
|
||||
const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated]
|
||||
await this.handleVaultRootKeyRotatedEvent(payload.vault)
|
||||
} else if (event.type === SyncEvent.ReceivedSharedVaultInvites) {
|
||||
await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData)
|
||||
} else if (event.type === SyncEvent.ReceivedRemoteSharedVaults) {
|
||||
void this.notifyCollaborationStatusChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleUserEvent(event: UserEventServiceEventPayload): Promise<void> {
|
||||
if (event.eventPayload.eventType === UserEventType.RemovedFromSharedVault) {
|
||||
const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: event.eventPayload.sharedVaultUuid })
|
||||
if (vault) {
|
||||
const useCase = new DeleteExternalSharedVaultUseCase(
|
||||
this.items,
|
||||
this.mutator,
|
||||
this.encryption,
|
||||
this.storage,
|
||||
this.sync,
|
||||
)
|
||||
await useCase.execute(vault)
|
||||
const vault = this.getVault.execute<SharedVaultListingInterface>({
|
||||
sharedVaultUuid: event.eventPayload.sharedVaultUuid,
|
||||
})
|
||||
if (!vault.isFailed()) {
|
||||
await this.deleteThirdPartyVault.execute(vault.getValue())
|
||||
}
|
||||
} else if (event.eventPayload.eventType === UserEventType.SharedVaultItemRemoved) {
|
||||
const item = this.items.findItem(event.eventPayload.itemUuid)
|
||||
@@ -176,15 +167,14 @@ export class SharedVaultService
|
||||
return
|
||||
}
|
||||
|
||||
const usecase = new NotifySharedVaultUsersOfRootKeyRotationUseCase(
|
||||
this.usersServer,
|
||||
this.invitesServer,
|
||||
this.messageServer,
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
)
|
||||
|
||||
await usecase.execute({ sharedVault: vault, userUuid: this.session.getSureUser().uuid })
|
||||
await this.notifyVaultUsersOfKeyRotation.execute({
|
||||
sharedVault: vault,
|
||||
senderUuid: this.session.getSureUser().uuid,
|
||||
keys: {
|
||||
encryption: this.encryption.getKeyPair(),
|
||||
signing: this.encryption.getSigningKeyPair(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async createSharedVault(dto: {
|
||||
@@ -193,16 +183,7 @@ export class SharedVaultService
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference?: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface | ClientDisplayableError> {
|
||||
const usecase = new CreateSharedVaultUseCase(
|
||||
this.encryption,
|
||||
this.items,
|
||||
this.mutator,
|
||||
this.sync,
|
||||
this.files,
|
||||
this.server,
|
||||
)
|
||||
|
||||
return usecase.execute({
|
||||
return this.createSharedVaultUseCase.execute({
|
||||
vaultName: dto.name,
|
||||
vaultDescription: dto.description,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
@@ -213,9 +194,7 @@ export class SharedVaultService
|
||||
async convertVaultToSharedVault(
|
||||
vault: VaultListingInterface,
|
||||
): Promise<SharedVaultListingInterface | ClientDisplayableError> {
|
||||
const usecase = new ConvertToSharedVaultUseCase(this.items, this.mutator, this.sync, this.files, this.server)
|
||||
|
||||
return usecase.execute({ vault })
|
||||
return this.convertToSharedVault.execute({ vault })
|
||||
}
|
||||
|
||||
public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] {
|
||||
@@ -228,7 +207,12 @@ export class SharedVaultService
|
||||
}
|
||||
|
||||
private findSharedVault(sharedVaultUuid: string): SharedVaultListingInterface | undefined {
|
||||
return this.getAllSharedVaults().find((vault) => vault.sharing.sharedVaultUuid === sharedVaultUuid)
|
||||
const result = this.getVault.execute<SharedVaultListingInterface>({ sharedVaultUuid })
|
||||
if (result.isFailed()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.getValue()
|
||||
}
|
||||
|
||||
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
|
||||
@@ -256,7 +240,11 @@ export class SharedVaultService
|
||||
|
||||
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
|
||||
for (const contact of contacts) {
|
||||
await this.shareContactWithUserAdministeredSharedVaults(contact)
|
||||
if (contact.isMe) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.shareContactWithVaults(contact)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,18 +254,13 @@ export class SharedVaultService
|
||||
continue
|
||||
}
|
||||
|
||||
const usecase = new SendSharedVaultMetadataChangedMessageToAll(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
this.usersServer,
|
||||
this.messageServer,
|
||||
)
|
||||
|
||||
await usecase.execute({
|
||||
await this.sendVaultDataChangeMessage.execute({
|
||||
vault,
|
||||
senderUuid: this.session.getSureUser().uuid,
|
||||
senderEncryptionKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
keys: {
|
||||
encryption: this.encryption.getKeyPair(),
|
||||
signing: this.encryption.getSigningKeyPair(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -326,8 +309,7 @@ export class SharedVaultService
|
||||
}
|
||||
|
||||
public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
|
||||
const useCase = new DeleteSharedVaultUseCase(this.server, this.items, this.mutator, this.sync, this.encryption)
|
||||
return useCase.execute({ sharedVault })
|
||||
return this.deleteSharedVaultUseCase.execute({ sharedVault })
|
||||
}
|
||||
|
||||
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
|
||||
@@ -342,39 +324,34 @@ export class SharedVaultService
|
||||
}
|
||||
|
||||
for (const invite of invites) {
|
||||
const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
)
|
||||
const sender = this.findContact.execute({ userUuid: invite.sender_uuid })
|
||||
if (!sender.isFailed()) {
|
||||
const trustedMessage = this.getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
|
||||
message: invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
sender: sender.getValue(),
|
||||
})
|
||||
|
||||
const trustedMessage = trustedMessageUseCase.execute({
|
||||
message: invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
})
|
||||
if (!trustedMessage.isFailed()) {
|
||||
this.pendingInvites[invite.uuid] = {
|
||||
invite,
|
||||
message: trustedMessage.getValue(),
|
||||
trusted: true,
|
||||
}
|
||||
|
||||
if (trustedMessage) {
|
||||
this.pendingInvites[invite.uuid] = {
|
||||
invite,
|
||||
message: trustedMessage,
|
||||
trusted: true,
|
||||
continue
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const untrustedMessageUseCase = new GetAsymmetricMessageUntrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
||||
this.encryption,
|
||||
)
|
||||
|
||||
const untrustedMessage = untrustedMessageUseCase.execute({
|
||||
const untrustedMessage = this.getUntrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
|
||||
message: invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
})
|
||||
|
||||
if (untrustedMessage) {
|
||||
if (!untrustedMessage.isFailed()) {
|
||||
this.pendingInvites[invite.uuid] = {
|
||||
invite,
|
||||
message: untrustedMessage,
|
||||
message: untrustedMessage.getValue(),
|
||||
trusted: false,
|
||||
}
|
||||
}
|
||||
@@ -392,8 +369,7 @@ export class SharedVaultService
|
||||
throw new Error('Cannot accept untrusted invite')
|
||||
}
|
||||
|
||||
const useCase = new AcceptTrustedSharedVaultInvite(this.invitesServer, this.mutator, this.sync, this.contacts)
|
||||
await useCase.execute({ invite: pendingInvite.invite, message: pendingInvite.message })
|
||||
await this.acceptVaultInvite.execute({ invite: pendingInvite.invite, message: pendingInvite.message })
|
||||
|
||||
delete this.pendingInvites[pendingInvite.invite.uuid]
|
||||
|
||||
@@ -416,35 +392,38 @@ export class SharedVaultService
|
||||
return []
|
||||
}
|
||||
|
||||
const contacts = this.contacts.getAllContacts()
|
||||
return contacts.filter((contact) => {
|
||||
const contacts = this.getAllContacts.execute()
|
||||
if (contacts.isFailed()) {
|
||||
return []
|
||||
}
|
||||
return contacts.getValue().filter((contact) => {
|
||||
const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid)
|
||||
return !isContactAlreadyInVault
|
||||
})
|
||||
}
|
||||
|
||||
private async getSharedVaultContacts(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]> {
|
||||
const usecase = new GetSharedVaultTrustedContacts(this.contacts, this.usersServer)
|
||||
const contacts = await usecase.execute(sharedVault)
|
||||
if (!contacts) {
|
||||
const contacts = await this.getVaultContacts.execute(sharedVault.sharing.sharedVaultUuid)
|
||||
if (contacts.isFailed()) {
|
||||
return []
|
||||
}
|
||||
|
||||
return contacts
|
||||
return contacts.getValue()
|
||||
}
|
||||
|
||||
async inviteContactToSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
contact: TrustedContactInterface,
|
||||
permissions: SharedVaultPermission,
|
||||
): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
): Promise<Result<SharedVaultInviteServerHash>> {
|
||||
const sharedVaultContacts = await this.getSharedVaultContacts(sharedVault)
|
||||
|
||||
const useCase = new InviteContactToSharedVaultUseCase(this.encryption, this.invitesServer)
|
||||
|
||||
const result = await useCase.execute({
|
||||
senderKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
const result = await this.inviteToVault.execute({
|
||||
keys: {
|
||||
encryption: this.encryption.getKeyPair(),
|
||||
signing: this.encryption.getSigningKeyPair(),
|
||||
},
|
||||
senderUuid: this.session.getSureUser().uuid,
|
||||
sharedVault,
|
||||
recipient: contact,
|
||||
sharedVaultContacts,
|
||||
@@ -470,8 +449,10 @@ export class SharedVaultService
|
||||
throw new Error('Cannot remove user from locked vault')
|
||||
}
|
||||
|
||||
const useCase = new RemoveVaultMemberUseCase(this.usersServer)
|
||||
const result = await useCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid, userUuid })
|
||||
const result = await this.removeVaultMember.execute({
|
||||
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
|
||||
userUuid,
|
||||
})
|
||||
if (isClientDisplayableError(result)) {
|
||||
return result
|
||||
}
|
||||
@@ -482,15 +463,7 @@ export class SharedVaultService
|
||||
}
|
||||
|
||||
async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
|
||||
const useCase = new LeaveVaultUseCase(
|
||||
this.usersServer,
|
||||
this.items,
|
||||
this.mutator,
|
||||
this.encryption,
|
||||
this.storage,
|
||||
this.sync,
|
||||
)
|
||||
const result = await useCase.execute({
|
||||
const result = await this.leaveVault.execute({
|
||||
sharedVault: sharedVault,
|
||||
userUuid: this.session.getSureUser().uuid,
|
||||
})
|
||||
@@ -505,28 +478,22 @@ export class SharedVaultService
|
||||
async getSharedVaultUsers(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
): Promise<SharedVaultUserServerHash[] | undefined> {
|
||||
const useCase = new GetSharedVaultUsersUseCase(this.usersServer)
|
||||
return useCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
|
||||
return this.getSharedVaultUsersUseCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
|
||||
}
|
||||
|
||||
private async shareContactWithUserAdministeredSharedVaults(contact: TrustedContactInterface): Promise<void> {
|
||||
const sharedVaults = this.getAllSharedVaults()
|
||||
async shareContactWithVaults(contact: TrustedContactInterface): Promise<void> {
|
||||
if (contact.isMe) {
|
||||
throw new Error('Cannot share self contact')
|
||||
}
|
||||
|
||||
const useCase = new ShareContactWithAllMembersOfSharedVaultUseCase(
|
||||
this.contacts,
|
||||
this.encryption,
|
||||
this.usersServer,
|
||||
this.messageServer,
|
||||
)
|
||||
const ownedVaults = this.getAllSharedVaults().filter(this.isCurrentUserSharedVaultAdmin.bind(this))
|
||||
|
||||
for (const vault of sharedVaults) {
|
||||
if (!this.isCurrentUserSharedVaultAdmin(vault)) {
|
||||
continue
|
||||
}
|
||||
|
||||
await useCase.execute({
|
||||
senderKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
for (const vault of ownedVaults) {
|
||||
await this.shareContactWithVault.execute({
|
||||
keys: {
|
||||
encryption: this.encryption.getKeyPair(),
|
||||
signing: this.encryption.getSigningKeyPair(),
|
||||
},
|
||||
sharedVault: vault,
|
||||
contactToShare: contact,
|
||||
senderUserUuid: this.session.getSureUser().uuid,
|
||||
@@ -539,9 +506,9 @@ export class SharedVaultService
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contact = this.contacts.findTrustedContact(item.last_edited_by_uuid)
|
||||
const contact = this.findContact.execute({ userUuid: item.last_edited_by_uuid })
|
||||
|
||||
return contact
|
||||
return contact.isFailed() ? undefined : contact.getValue()
|
||||
}
|
||||
|
||||
getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined {
|
||||
@@ -549,23 +516,37 @@ export class SharedVaultService
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contact = this.contacts.findTrustedContact(item.user_uuid)
|
||||
const contact = this.findContact.execute({ userUuid: item.user_uuid })
|
||||
|
||||
return contact
|
||||
return contact.isFailed() ? undefined : contact.getValue()
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.contacts as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.files as unknown) = undefined
|
||||
;(this.invitesServer as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.messageServer as unknown) = undefined
|
||||
;(this.server as unknown) = undefined
|
||||
;(this.session as unknown) = undefined
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.usersServer as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.session as unknown) = undefined
|
||||
;(this.vaults as unknown) = undefined
|
||||
;(this.invitesServer as unknown) = undefined
|
||||
;(this.getVault as unknown) = undefined
|
||||
;(this.createSharedVaultUseCase as unknown) = undefined
|
||||
;(this.handleKeyPairChange as unknown) = undefined
|
||||
;(this.notifyVaultUsersOfKeyRotation as unknown) = undefined
|
||||
;(this.sendVaultDataChangeMessage as unknown) = undefined
|
||||
;(this.getTrustedPayload as unknown) = undefined
|
||||
;(this.getUntrustedPayload as unknown) = undefined
|
||||
;(this.findContact as unknown) = undefined
|
||||
;(this.getAllContacts as unknown) = undefined
|
||||
;(this.getVaultContacts as unknown) = undefined
|
||||
;(this.acceptVaultInvite as unknown) = undefined
|
||||
;(this.inviteToVault as unknown) = undefined
|
||||
;(this.leaveVault as unknown) = undefined
|
||||
;(this.deleteThirdPartyVault as unknown) = undefined
|
||||
;(this.shareContactWithVault as unknown) = undefined
|
||||
;(this.convertToSharedVault as unknown) = undefined
|
||||
;(this.deleteSharedVaultUseCase as unknown) = undefined
|
||||
;(this.removeVaultMember as unknown) = undefined
|
||||
;(this.getSharedVaultUsersUseCase as unknown) = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
|
||||
import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
export interface SharedVaultServiceInterface
|
||||
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload> {
|
||||
@@ -31,7 +32,7 @@ export interface SharedVaultServiceInterface
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
contact: TrustedContactInterface,
|
||||
permissions: SharedVaultPermission,
|
||||
): Promise<SharedVaultInviteServerHash | ClientDisplayableError>
|
||||
): Promise<Result<SharedVaultInviteServerHash>>
|
||||
removeUserFromSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
userUuid: string,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
|
||||
import { HandleTrustedSharedVaultInviteMessage } from '../../AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
|
||||
export class AcceptTrustedSharedVaultInvite {
|
||||
constructor(
|
||||
private vaultInvitesServer: SharedVaultInvitesServerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
invite: SharedVaultInviteServerHash
|
||||
message: AsymmetricMessageSharedVaultInvite
|
||||
}): Promise<void> {
|
||||
const useCase = new HandleTrustedSharedVaultInviteMessage(this.mutator, this.sync, this.contacts)
|
||||
await useCase.execute(dto.message, dto.invite.shared_vault_uuid, dto.invite.sender_uuid)
|
||||
|
||||
await this.vaultInvitesServer.acceptInvite({
|
||||
sharedVaultUuid: dto.invite.shared_vault_uuid,
|
||||
inviteUuid: dto.invite.uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
|
||||
import { ProcessAcceptedVaultInvite } from '../../AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite'
|
||||
|
||||
export class AcceptVaultInvite {
|
||||
constructor(
|
||||
private inviteServer: SharedVaultInvitesServerInterface,
|
||||
private processInvite: ProcessAcceptedVaultInvite,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
invite: SharedVaultInviteServerHash
|
||||
message: AsymmetricMessageSharedVaultInvite
|
||||
}): Promise<void> {
|
||||
await this.processInvite.execute(dto.message, dto.invite.shared_vault_uuid, dto.invite.sender_uuid)
|
||||
|
||||
await this.inviteServer.acceptInvite({
|
||||
sharedVaultUuid: dto.invite.shared_vault_uuid,
|
||||
inviteUuid: dto.invite.uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { SharedVaultListingInterface, VaultListingInterface, VaultListingMutator } from '@standardnotes/models'
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultServerInterface } from '@standardnotes/api'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { MoveItemsToVaultUseCase } from '../../Vaults/UseCase/MoveItemsToVault'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { MoveItemsToVault } from '../../Vaults/UseCase/MoveItemsToVault'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class ConvertToSharedVaultUseCase {
|
||||
export class ConvertToSharedVault {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
private moveItemsToVault: MoveItemsToVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: { vault: VaultListingInterface }): Promise<SharedVaultListingInterface | ClientDisplayableError> {
|
||||
@@ -39,8 +36,8 @@ export class ConvertToSharedVaultUseCase {
|
||||
)
|
||||
|
||||
const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier)
|
||||
const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files)
|
||||
await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
await this.moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
return sharedVaultListing as SharedVaultListingInterface
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import {
|
||||
KeySystemRootKeyStorageMode,
|
||||
SharedVaultListingInterface,
|
||||
@@ -6,22 +5,19 @@ import {
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { SharedVaultServerInterface } from '@standardnotes/api'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { CreateVaultUseCase } from '../../Vaults/UseCase/CreateVault'
|
||||
import { MoveItemsToVaultUseCase } from '../../Vaults/UseCase/MoveItemsToVault'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { CreateVault } from '../../Vaults/UseCase/CreateVault'
|
||||
import { MoveItemsToVault } from '../../Vaults/UseCase/MoveItemsToVault'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class CreateSharedVaultUseCase {
|
||||
export class CreateSharedVault {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
private createVault: CreateVault,
|
||||
private moveItemsToVault: MoveItemsToVault,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
@@ -30,8 +26,7 @@ export class CreateSharedVaultUseCase {
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<SharedVaultListingInterface | ClientDisplayableError> {
|
||||
const usecase = new CreateVaultUseCase(this.mutator, this.encryption, this.sync)
|
||||
const privateVault = await usecase.execute({
|
||||
const privateVault = await this.createVault.execute({
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
@@ -56,8 +51,8 @@ export class CreateSharedVaultUseCase {
|
||||
)
|
||||
|
||||
const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier)
|
||||
const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files)
|
||||
await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
await this.moveItemsToVault.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
return sharedVaultListing as SharedVaultListingInterface
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { StorageServiceInterface } from '../../Storage/StorageServiceInterface'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
import { RemoveItemsLocallyUseCase } from '../../UseCase/RemoveItemsLocally'
|
||||
|
||||
export class DeleteExternalSharedVaultUseCase {
|
||||
private removeItemsLocallyUsecase = new RemoveItemsLocallyUseCase(this.items, this.storage)
|
||||
import { RemoveItemsLocally } from '../../UseCase/RemoveItemsLocally'
|
||||
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class DeleteThirdPartyVault {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private keys: KeySystemKeyManagerInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private removeItemsLocally: RemoveItemsLocally,
|
||||
) {}
|
||||
|
||||
async execute(vault: VaultListingInterface): Promise<void> {
|
||||
await this.deleteDataSharedByVaultUsers(vault)
|
||||
await this.deleteDataOwnedByThisUser(vault)
|
||||
await this.encryption.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier)
|
||||
await this.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier)
|
||||
|
||||
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
|
||||
}
|
||||
@@ -34,13 +31,13 @@ export class DeleteExternalSharedVaultUseCase {
|
||||
this.items.allTrackedItems().filter((item) => item.key_system_identifier === vault.systemIdentifier)
|
||||
)
|
||||
|
||||
const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||
const itemsKeys = this.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||
|
||||
await this.removeItemsLocallyUsecase.execute([...vaultItems, ...itemsKeys])
|
||||
await this.removeItemsLocally.execute([...vaultItems, ...itemsKeys])
|
||||
}
|
||||
|
||||
private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise<void> {
|
||||
const rootKeys = this.encryption.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
const rootKeys = this.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(rootKeys)
|
||||
|
||||
await this.mutator.setItemToBeDeleted(vault)
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultServerInterface } from '@standardnotes/api'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { DeleteVaultUseCase } from '../../Vaults/UseCase/DeleteVault'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { DeleteVault } from '../../Vaults/UseCase/DeleteVault'
|
||||
|
||||
export class DeleteSharedVaultUseCase {
|
||||
export class DeleteSharedVault {
|
||||
constructor(
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private deleteVault: DeleteVault,
|
||||
) {}
|
||||
|
||||
async execute(params: { sharedVault: SharedVaultListingInterface }): Promise<ClientDisplayableError | void> {
|
||||
@@ -25,8 +20,7 @@ export class DeleteSharedVaultUseCase {
|
||||
return ClientDisplayableError.FromString(`Failed to delete vault ${response}`)
|
||||
}
|
||||
|
||||
const deleteUsecase = new DeleteVaultUseCase(this.items, this.mutator, this.encryption)
|
||||
await deleteUsecase.execute(params.sharedVault)
|
||||
await this.deleteVault.execute(params.sharedVault)
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers'
|
||||
import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { isNotUndefined } from '@standardnotes/utils'
|
||||
|
||||
export class GetSharedVaultTrustedContacts {
|
||||
constructor(
|
||||
private contacts: ContactServiceInterface,
|
||||
private sharedVaultUsersServer: SharedVaultUsersServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(vault: SharedVaultListingInterface): Promise<TrustedContactInterface[] | undefined> {
|
||||
const useCase = new GetSharedVaultUsersUseCase(this.sharedVaultUsersServer)
|
||||
const users = await useCase.execute({ sharedVaultUuid: vault.sharing.sharedVaultUuid })
|
||||
if (!users) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contacts = users.map((user) => this.contacts.findTrustedContact(user.user_uuid)).filter(isNotUndefined)
|
||||
return contacts
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { GetVaultUsers } from './GetVaultUsers'
|
||||
import { TrustedContactInterface } from '@standardnotes/models'
|
||||
import { isNotUndefined } from '@standardnotes/utils'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class GetVaultContacts implements UseCaseInterface<TrustedContactInterface[]> {
|
||||
constructor(private findContact: FindContact, private getVaultUsers: GetVaultUsers) {}
|
||||
|
||||
async execute(sharedVaultUuid: string): Promise<Result<TrustedContactInterface[]>> {
|
||||
const users = await this.getVaultUsers.execute({ sharedVaultUuid })
|
||||
if (!users) {
|
||||
return Result.fail('Failed to get vault users')
|
||||
}
|
||||
|
||||
const contacts = users
|
||||
.map((user) => this.findContact.execute({ userUuid: user.user_uuid }))
|
||||
.map((result) => (result.isFailed() ? undefined : result.getValue()))
|
||||
.filter(isNotUndefined)
|
||||
|
||||
return Result.ok(contacts)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SharedVaultUserServerHash, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class GetSharedVaultUsersUseCase {
|
||||
export class GetVaultUsers {
|
||||
constructor(private vaultUsersServer: SharedVaultUsersServerInterface) {}
|
||||
|
||||
async execute(params: { sharedVaultUuid: string }): Promise<SharedVaultUserServerHash[] | undefined> {
|
||||
@@ -1,63 +0,0 @@
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ClientDisplayableError, SharedVaultInviteServerHash, SharedVaultPermission } from '@standardnotes/responses'
|
||||
import {
|
||||
TrustedContactInterface,
|
||||
SharedVaultListingInterface,
|
||||
AsymmetricMessagePayloadType,
|
||||
} from '@standardnotes/models'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { SendSharedVaultInviteUseCase } from './SendSharedVaultInviteUseCase'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export class InviteContactToSharedVaultUseCase {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private sharedVaultInviteServer: SharedVaultInvitesServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
sharedVault: SharedVaultListingInterface
|
||||
sharedVaultContacts: TrustedContactInterface[]
|
||||
recipient: TrustedContactInterface
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.sharedVault.systemIdentifier)
|
||||
if (!keySystemRootKey) {
|
||||
return ClientDisplayableError.FromString('Cannot add contact; key system root key not found')
|
||||
}
|
||||
|
||||
const delegatedContacts = params.sharedVaultContacts.filter(
|
||||
(contact) => !contact.isMe && contact.contactUuid !== params.recipient.contactUuid,
|
||||
)
|
||||
|
||||
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
|
||||
message: {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultInvite,
|
||||
data: {
|
||||
recipientUuid: params.recipient.contactUuid,
|
||||
rootKey: keySystemRootKey.content,
|
||||
trustedContacts: delegatedContacts.map((contact) => contact.content),
|
||||
metadata: {
|
||||
name: params.sharedVault.name,
|
||||
description: params.sharedVault.description,
|
||||
},
|
||||
},
|
||||
},
|
||||
senderKeyPair: params.senderKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
recipientPublicKey: params.recipient.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
const createInviteUseCase = new SendSharedVaultInviteUseCase(this.sharedVaultInviteServer)
|
||||
const createInviteResult = await createInviteUseCase.execute({
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
recipientUuid: params.recipient.contactUuid,
|
||||
encryptedMessage,
|
||||
permissions: params.permissions,
|
||||
})
|
||||
|
||||
return createInviteResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { SharedVaultInviteServerHash, SharedVaultPermission } from '@standardnotes/responses'
|
||||
import {
|
||||
TrustedContactInterface,
|
||||
SharedVaultListingInterface,
|
||||
AsymmetricMessagePayloadType,
|
||||
VaultInviteDelegatedContact,
|
||||
} from '@standardnotes/models'
|
||||
import { SendVaultInvite } from './SendVaultInvite'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ShareContactWithVault } from './ShareContactWithVault'
|
||||
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHash> {
|
||||
constructor(
|
||||
private keyManager: KeySystemKeyManagerInterface,
|
||||
private encryptMessage: EncryptMessage,
|
||||
private sendInvite: SendVaultInvite,
|
||||
private shareContact: ShareContactWithVault,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
senderUuid: string
|
||||
sharedVault: SharedVaultListingInterface
|
||||
sharedVaultContacts: TrustedContactInterface[]
|
||||
recipient: TrustedContactInterface
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<Result<SharedVaultInviteServerHash>> {
|
||||
const createInviteResult = await this.inviteContact(params)
|
||||
|
||||
if (createInviteResult.isFailed()) {
|
||||
return createInviteResult
|
||||
}
|
||||
|
||||
await this.shareContactWithOtherVaultMembers({
|
||||
contact: params.recipient,
|
||||
senderUuid: params.senderUuid,
|
||||
keys: params.keys,
|
||||
sharedVault: params.sharedVault,
|
||||
})
|
||||
|
||||
return createInviteResult
|
||||
}
|
||||
|
||||
private async shareContactWithOtherVaultMembers(params: {
|
||||
contact: TrustedContactInterface
|
||||
senderUuid: string
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
sharedVault: SharedVaultListingInterface
|
||||
}): Promise<Result<void>> {
|
||||
const result = await this.shareContact.execute({
|
||||
keys: params.keys,
|
||||
senderUserUuid: params.senderUuid,
|
||||
sharedVault: params.sharedVault,
|
||||
contactToShare: params.contact,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async inviteContact(params: {
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
sharedVault: SharedVaultListingInterface
|
||||
sharedVaultContacts: TrustedContactInterface[]
|
||||
recipient: TrustedContactInterface
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<Result<SharedVaultInviteServerHash>> {
|
||||
const keySystemRootKey = this.keyManager.getPrimaryKeySystemRootKey(params.sharedVault.systemIdentifier)
|
||||
if (!keySystemRootKey) {
|
||||
return Result.fail('Cannot invite contact; key system root key not found')
|
||||
}
|
||||
|
||||
const meContact = params.sharedVaultContacts.find((contact) => contact.isMe)
|
||||
if (!meContact) {
|
||||
return Result.fail('Cannot invite contact; me contact not found')
|
||||
}
|
||||
|
||||
const meContactContent: VaultInviteDelegatedContact = {
|
||||
name: undefined,
|
||||
contactUuid: meContact.contactUuid,
|
||||
publicKeySet: meContact.publicKeySet,
|
||||
}
|
||||
|
||||
const delegatedContacts: VaultInviteDelegatedContact[] = params.sharedVaultContacts
|
||||
.filter((contact) => !contact.isMe && contact.contactUuid !== params.recipient.contactUuid)
|
||||
.map((contact) => {
|
||||
return {
|
||||
name: contact.name,
|
||||
contactUuid: contact.contactUuid,
|
||||
publicKeySet: contact.publicKeySet,
|
||||
}
|
||||
})
|
||||
|
||||
const encryptedMessage = this.encryptMessage.execute({
|
||||
message: {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultInvite,
|
||||
data: {
|
||||
recipientUuid: params.recipient.contactUuid,
|
||||
rootKey: keySystemRootKey.content,
|
||||
trustedContacts: [meContactContent, ...delegatedContacts],
|
||||
metadata: {
|
||||
name: params.sharedVault.name,
|
||||
description: params.sharedVault.description,
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: params.keys,
|
||||
recipientPublicKey: params.recipient.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (encryptedMessage.isFailed()) {
|
||||
return Result.fail(encryptedMessage.getError())
|
||||
}
|
||||
|
||||
const createInviteResult = await this.sendInvite.execute({
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
recipientUuid: params.recipient.contactUuid,
|
||||
encryptedMessage: encryptedMessage.getValue(),
|
||||
permissions: params.permissions,
|
||||
})
|
||||
|
||||
return createInviteResult
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { StorageServiceInterface } from './../../Storage/StorageServiceInterface'
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { DeleteExternalSharedVaultUseCase } from './DeleteExternalSharedVault'
|
||||
import { DeleteThirdPartyVault } from './DeleteExternalSharedVault'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
|
||||
export class LeaveVaultUseCase {
|
||||
export class LeaveVault {
|
||||
constructor(
|
||||
private vaultUserServer: SharedVaultUsersServerInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private deleteThirdPartyVault: DeleteThirdPartyVault,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
@@ -36,13 +29,6 @@ export class LeaveVaultUseCase {
|
||||
return ClientDisplayableError.FromString(`Failed to leave vault ${JSON.stringify(response)}`)
|
||||
}
|
||||
|
||||
const removeLocalItems = new DeleteExternalSharedVaultUseCase(
|
||||
this.items,
|
||||
this.mutator,
|
||||
this.encryption,
|
||||
this.storage,
|
||||
this.sync,
|
||||
)
|
||||
await removeLocalItems.execute(latestVaultListing)
|
||||
await this.deleteThirdPartyVault.execute(latestVaultListing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
AsymmetricMessageServerInterface,
|
||||
SharedVaultInvitesServerInterface,
|
||||
SharedVaultUsersServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { ReuploadSharedVaultInvitesAfterKeyRotationUseCase } from './ReuploadSharedVaultInvitesAfterKeyRotation'
|
||||
import { SendSharedVaultRootKeyChangedMessageToAll } from './SendSharedVaultRootKeyChangedMessageToAll'
|
||||
|
||||
export class NotifySharedVaultUsersOfRootKeyRotationUseCase {
|
||||
constructor(
|
||||
private sharedVaultUsersServer: SharedVaultUsersServerInterface,
|
||||
private sharedVaultInvitesServer: SharedVaultInvitesServerInterface,
|
||||
private messageServer: AsymmetricMessageServerInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
sharedVault: SharedVaultListingInterface
|
||||
userUuid: string
|
||||
}): Promise<ClientDisplayableError[]> {
|
||||
const errors: ClientDisplayableError[] = []
|
||||
const updatePendingInvitesUseCase = new ReuploadSharedVaultInvitesAfterKeyRotationUseCase(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
this.sharedVaultInvitesServer,
|
||||
this.sharedVaultUsersServer,
|
||||
)
|
||||
|
||||
const updateExistingResults = await updatePendingInvitesUseCase.execute({
|
||||
sharedVault: params.sharedVault,
|
||||
senderUuid: params.userUuid,
|
||||
senderEncryptionKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
})
|
||||
|
||||
errors.push(...updateExistingResults)
|
||||
|
||||
const shareKeyUseCase = new SendSharedVaultRootKeyChangedMessageToAll(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
this.sharedVaultUsersServer,
|
||||
this.messageServer,
|
||||
)
|
||||
|
||||
const shareKeyResults = await shareKeyUseCase.execute({
|
||||
keySystemIdentifier: params.sharedVault.systemIdentifier,
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
senderUuid: params.userUuid,
|
||||
senderEncryptionKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
})
|
||||
|
||||
errors.push(...shareKeyResults)
|
||||
|
||||
return errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { AsymmetricMessageSharedVaultInvite, SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SendVaultKeyChangedMessage } from './SendVaultKeyChangedMessage'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { InviteToVault } from './InviteToVault'
|
||||
import { GetVaultContacts } from './GetVaultContacts'
|
||||
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
|
||||
type Params = {
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
sharedVault: SharedVaultListingInterface
|
||||
senderUuid: string
|
||||
}
|
||||
|
||||
export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private findContact: FindContact,
|
||||
private sendKeyChangedMessage: SendVaultKeyChangedMessage,
|
||||
private inviteToVault: InviteToVault,
|
||||
private inviteServer: SharedVaultInvitesServerInterface,
|
||||
private getVaultContacts: GetVaultContacts,
|
||||
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessageSharedVaultInvite>,
|
||||
) {}
|
||||
|
||||
async execute(params: Params): Promise<Result<void>> {
|
||||
await this.reinvitePendingInvites(params)
|
||||
|
||||
await this.performSendKeyChangeMessage(params)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async reinvitePendingInvites(params: Params): Promise<Result<void>> {
|
||||
const existingInvites = await this.getExistingInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
if (existingInvites.isFailed()) {
|
||||
return existingInvites
|
||||
}
|
||||
|
||||
await this.deleteAllInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
|
||||
const contacts = await this.getVaultContacts.execute(params.sharedVault.sharing.sharedVaultUuid)
|
||||
|
||||
for (const invite of existingInvites.getValue()) {
|
||||
const recipient = this.findContact.execute({ userUuid: invite.user_uuid })
|
||||
if (recipient.isFailed()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const decryptedPreviousInvite = this.decryptOwnMessage.execute({
|
||||
message: invite.encrypted_message,
|
||||
privateKey: params.keys.encryption.privateKey,
|
||||
recipientPublicKey: recipient.getValue().publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (decryptedPreviousInvite.isFailed()) {
|
||||
return Result.fail(decryptedPreviousInvite.getError())
|
||||
}
|
||||
|
||||
await this.inviteToVault.execute({
|
||||
keys: params.keys,
|
||||
sharedVault: params.sharedVault,
|
||||
sharedVaultContacts: !contacts.isFailed() ? contacts.getValue() : [],
|
||||
recipient: recipient.getValue(),
|
||||
permissions: invite.permissions,
|
||||
senderUuid: params.senderUuid,
|
||||
})
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async performSendKeyChangeMessage(params: Params): Promise<Result<void>> {
|
||||
const result = await this.sendKeyChangedMessage.execute({
|
||||
keySystemIdentifier: params.sharedVault.systemIdentifier,
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
senderUuid: params.senderUuid,
|
||||
keys: params.keys,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
return result
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async deleteAllInvites(sharedVaultUuid: string): Promise<Result<void>> {
|
||||
const response = await this.inviteServer.deleteAllSharedVaultInvites({
|
||||
sharedVaultUuid: sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to delete existing invites ${response}`)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async getExistingInvites(sharedVaultUuid: string): Promise<Result<SharedVaultInviteServerHash[]>> {
|
||||
const response = await this.inviteServer.getOutboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to get outbound user invites ${response}`)
|
||||
}
|
||||
|
||||
const invites = response.data.invites
|
||||
|
||||
return Result.ok(invites.filter((invite) => invite.shared_vault_uuid === sharedVaultUuid))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class RemoveVaultMemberUseCase {
|
||||
export class RemoveVaultMember {
|
||||
constructor(private vaultUserServer: SharedVaultUsersServerInterface) {}
|
||||
|
||||
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<ClientDisplayableError | void> {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ReuploadInvite } from './ReuploadInvite'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
|
||||
type ReuploadAllInvitesDTO = {
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
previousKeys?: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}
|
||||
|
||||
export class ReuploadAllInvites implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private reuploadInvite: ReuploadInvite,
|
||||
private findContact: FindContact,
|
||||
private inviteServer: SharedVaultInvitesServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: ReuploadAllInvitesDTO): Promise<Result<void>> {
|
||||
const invites = await this.getExistingInvites()
|
||||
if (invites.isFailed()) {
|
||||
return invites
|
||||
}
|
||||
|
||||
const deleteResult = await this.deleteExistingInvites()
|
||||
if (deleteResult.isFailed()) {
|
||||
return deleteResult
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
|
||||
for (const invite of invites.getValue()) {
|
||||
const recipient = this.findContact.execute({ userUuid: invite.user_uuid })
|
||||
if (recipient.isFailed()) {
|
||||
errors.push(`Contact not found for invite ${invite.user_uuid}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.reuploadInvite.execute({
|
||||
keys: params.keys,
|
||||
previousKeys: params.previousKeys,
|
||||
recipient: recipient.getValue(),
|
||||
previousInvite: invite,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
errors.push(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(errors.join(', '))
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async getExistingInvites(): Promise<Result<SharedVaultInviteServerHash[]>> {
|
||||
const response = await this.inviteServer.getOutboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to get outbound user invites ${response}`)
|
||||
}
|
||||
|
||||
const invites = response.data.invites
|
||||
|
||||
return Result.ok(invites)
|
||||
}
|
||||
|
||||
private async deleteExistingInvites(): Promise<Result<void>> {
|
||||
const response = await this.inviteServer.deleteAllOutboundInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to delete existing invites ${response}`)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { DecryptOwnMessage } from './../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
|
||||
import { AsymmetricMessageSharedVaultInvite, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { SendVaultInvite } from './SendVaultInvite'
|
||||
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
|
||||
|
||||
export class ReuploadInvite implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private decryptOwnMessage: DecryptOwnMessage<AsymmetricMessageSharedVaultInvite>,
|
||||
private sendInvite: SendVaultInvite,
|
||||
private encryptMessage: EncryptMessage,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
previousKeys?: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
recipient: TrustedContactInterface
|
||||
previousInvite: SharedVaultInviteServerHash
|
||||
}): Promise<Result<SharedVaultInviteServerHash>> {
|
||||
const decryptedPreviousInvite = this.decryptOwnMessage.execute({
|
||||
message: params.previousInvite.encrypted_message,
|
||||
privateKey: params.previousKeys?.encryption.privateKey ?? params.keys.encryption.privateKey,
|
||||
recipientPublicKey: params.recipient.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (decryptedPreviousInvite.isFailed()) {
|
||||
return Result.fail(decryptedPreviousInvite.getError())
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryptMessage.execute({
|
||||
message: decryptedPreviousInvite.getValue(),
|
||||
keys: params.keys,
|
||||
recipientPublicKey: params.recipient.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (encryptedMessage.isFailed()) {
|
||||
return Result.fail(encryptedMessage.getError())
|
||||
}
|
||||
|
||||
const createInviteResult = await this.sendInvite.execute({
|
||||
sharedVaultUuid: params.previousInvite.shared_vault_uuid,
|
||||
recipientUuid: params.recipient.contactUuid,
|
||||
encryptedMessage: encryptedMessage.getValue(),
|
||||
permissions: params.previousInvite.permissions,
|
||||
})
|
||||
|
||||
return createInviteResult
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
KeySystemRootKeyContentSpecialized,
|
||||
SharedVaultListingInterface,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
isClientDisplayableError,
|
||||
isErrorResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { InviteContactToSharedVaultUseCase } from './InviteContactToSharedVault'
|
||||
import { GetSharedVaultTrustedContacts } from './GetSharedVaultTrustedContacts'
|
||||
|
||||
type ReuploadAllSharedVaultInvitesDTO = {
|
||||
sharedVault: SharedVaultListingInterface
|
||||
senderUuid: string
|
||||
senderEncryptionKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
}
|
||||
|
||||
export class ReuploadSharedVaultInvitesAfterKeyRotationUseCase {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
private vaultInvitesServer: SharedVaultInvitesServerInterface,
|
||||
private vaultUserServer: SharedVaultUsersServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: ReuploadAllSharedVaultInvitesDTO): Promise<ClientDisplayableError[]> {
|
||||
const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.sharedVault.systemIdentifier)
|
||||
if (!keySystemRootKey) {
|
||||
throw new Error(`Vault key not found for keySystemIdentifier ${params.sharedVault.systemIdentifier}`)
|
||||
}
|
||||
|
||||
const existingInvites = await this.getExistingInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
if (isClientDisplayableError(existingInvites)) {
|
||||
return [existingInvites]
|
||||
}
|
||||
|
||||
const deleteResult = await this.deleteExistingInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
if (isClientDisplayableError(deleteResult)) {
|
||||
return [deleteResult]
|
||||
}
|
||||
|
||||
const vaultContacts = await this.getVaultContacts(params.sharedVault)
|
||||
if (vaultContacts.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const errors: ClientDisplayableError[] = []
|
||||
|
||||
for (const invite of existingInvites) {
|
||||
const contact = this.contacts.findTrustedContact(invite.user_uuid)
|
||||
if (!contact) {
|
||||
errors.push(ClientDisplayableError.FromString(`Contact not found for invite ${invite.user_uuid}`))
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.sendNewInvite({
|
||||
usecaseDTO: params,
|
||||
contact: contact,
|
||||
previousInvite: invite,
|
||||
keySystemRootKeyData: keySystemRootKey.content,
|
||||
sharedVaultContacts: vaultContacts,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(result)) {
|
||||
errors.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private async getVaultContacts(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]> {
|
||||
const usecase = new GetSharedVaultTrustedContacts(this.contacts, this.vaultUserServer)
|
||||
const contacts = await usecase.execute(sharedVault)
|
||||
if (!contacts) {
|
||||
return []
|
||||
}
|
||||
|
||||
return contacts
|
||||
}
|
||||
|
||||
private async getExistingInvites(
|
||||
sharedVaultUuid: string,
|
||||
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError> {
|
||||
const response = await this.vaultInvitesServer.getOutboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`)
|
||||
}
|
||||
|
||||
const invites = response.data.invites
|
||||
|
||||
return invites.filter((invite) => invite.shared_vault_uuid === sharedVaultUuid)
|
||||
}
|
||||
|
||||
private async deleteExistingInvites(sharedVaultUuid: string): Promise<ClientDisplayableError | void> {
|
||||
const response = await this.vaultInvitesServer.deleteAllSharedVaultInvites({
|
||||
sharedVaultUuid: sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromString(`Failed to delete existing invites ${response}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async sendNewInvite(params: {
|
||||
usecaseDTO: ReuploadAllSharedVaultInvitesDTO
|
||||
contact: TrustedContactInterface
|
||||
previousInvite: SharedVaultInviteServerHash
|
||||
keySystemRootKeyData: KeySystemRootKeyContentSpecialized
|
||||
sharedVaultContacts: TrustedContactInterface[]
|
||||
}): Promise<ClientDisplayableError | void> {
|
||||
const signatureResult = this.encryption.asymmetricSignatureVerifyDetached(params.previousInvite.encrypted_message)
|
||||
if (!signatureResult.signatureVerified) {
|
||||
return ClientDisplayableError.FromString('Failed to verify signature of previous invite')
|
||||
}
|
||||
|
||||
if (signatureResult.signaturePublicKey !== params.usecaseDTO.senderSigningKeyPair.publicKey) {
|
||||
return ClientDisplayableError.FromString('Sender public key does not match signature')
|
||||
}
|
||||
|
||||
const usecase = new InviteContactToSharedVaultUseCase(this.encryption, this.vaultInvitesServer)
|
||||
const result = await usecase.execute({
|
||||
senderKeyPair: params.usecaseDTO.senderEncryptionKeyPair,
|
||||
senderSigningKeyPair: params.usecaseDTO.senderSigningKeyPair,
|
||||
sharedVault: params.usecaseDTO.sharedVault,
|
||||
sharedVaultContacts: params.sharedVaultContacts,
|
||||
recipient: params.contact,
|
||||
permissions: params.previousInvite.permissions,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ReuploadInvite } from './ReuploadInvite'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
|
||||
type ReuploadVaultInvitesDTO = {
|
||||
sharedVault: SharedVaultListingInterface
|
||||
senderUuid: string
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}
|
||||
|
||||
export class ReuploadVaultInvites implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private reuploadInvite: ReuploadInvite,
|
||||
private findContact: FindContact,
|
||||
private inviteServer: SharedVaultInvitesServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: ReuploadVaultInvitesDTO): Promise<Result<void>> {
|
||||
const existingInvites = await this.getExistingInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
if (existingInvites.isFailed()) {
|
||||
return existingInvites
|
||||
}
|
||||
|
||||
const deleteResult = await this.deleteExistingInvites(params.sharedVault.sharing.sharedVaultUuid)
|
||||
if (deleteResult.isFailed()) {
|
||||
return deleteResult
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
|
||||
for (const invite of existingInvites.getValue()) {
|
||||
const recipient = this.findContact.execute({ userUuid: invite.user_uuid })
|
||||
if (recipient.isFailed()) {
|
||||
errors.push(`Contact not found for invite ${invite.user_uuid}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.reuploadInvite.execute({
|
||||
keys: params.keys,
|
||||
recipient: recipient.getValue(),
|
||||
previousInvite: invite,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
errors.push(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(errors.join(', '))
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async getExistingInvites(sharedVaultUuid: string): Promise<Result<SharedVaultInviteServerHash[]>> {
|
||||
const response = await this.inviteServer.getOutboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to get outbound user invites ${response}`)
|
||||
}
|
||||
|
||||
const invites = response.data.invites
|
||||
|
||||
return Result.ok(invites.filter((invite) => invite.shared_vault_uuid === sharedVaultUuid))
|
||||
}
|
||||
|
||||
private async deleteExistingInvites(sharedVaultUuid: string): Promise<Result<void>> {
|
||||
const response = await this.inviteServer.deleteAllSharedVaultInvites({
|
||||
sharedVaultUuid: sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return Result.fail(`Failed to delete existing invites ${response}`)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSharedVaultMetadataChanged,
|
||||
SharedVaultListingInterface,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
|
||||
import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase'
|
||||
|
||||
export class SendSharedVaultMetadataChangedMessageToAll {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
private vaultUsersServer: SharedVaultUsersServerInterface,
|
||||
private messageServer: AsymmetricMessageServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
vault: SharedVaultListingInterface
|
||||
senderUuid: string
|
||||
senderEncryptionKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
}): Promise<ClientDisplayableError[]> {
|
||||
const errors: ClientDisplayableError[] = []
|
||||
|
||||
const getUsersUseCase = new GetSharedVaultUsersUseCase(this.vaultUsersServer)
|
||||
const users = await getUsersUseCase.execute({ sharedVaultUuid: params.vault.sharing.sharedVaultUuid })
|
||||
if (!users) {
|
||||
return [ClientDisplayableError.FromString('Cannot send metadata changed message; users not found')]
|
||||
}
|
||||
|
||||
for (const user of users) {
|
||||
if (user.user_uuid === params.senderUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
const trustedContact = this.contacts.findTrustedContact(user.user_uuid)
|
||||
if (!trustedContact) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sendMessageResult = await this.sendToContact({
|
||||
vault: params.vault,
|
||||
senderKeyPair: params.senderEncryptionKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
contact: trustedContact,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(sendMessageResult)) {
|
||||
errors.push(sendMessageResult)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private async sendToContact(params: {
|
||||
vault: SharedVaultListingInterface
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
contact: TrustedContactInterface
|
||||
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
|
||||
const message: AsymmetricMessageSharedVaultMetadataChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
|
||||
data: {
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
sharedVaultUuid: params.vault.sharing.sharedVaultUuid,
|
||||
name: params.vault.name,
|
||||
description: params.vault.description,
|
||||
},
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
|
||||
message: message,
|
||||
senderKeyPair: params.senderKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
recipientPublicKey: params.contact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
const replaceabilityIdentifier = [
|
||||
AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
|
||||
params.vault.sharing.sharedVaultUuid,
|
||||
params.vault.systemIdentifier,
|
||||
].join(':')
|
||||
|
||||
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
|
||||
const sendMessageResult = await sendMessageUseCase.execute({
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
encryptedMessage,
|
||||
replaceabilityIdentifier,
|
||||
})
|
||||
|
||||
return sendMessageResult
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSharedVaultRootKeyChanged,
|
||||
KeySystemIdentifier,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
|
||||
import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase'
|
||||
|
||||
export class SendSharedVaultRootKeyChangedMessageToAll {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
private vaultUsersServer: SharedVaultUsersServerInterface,
|
||||
private messageServer: AsymmetricMessageServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
sharedVaultUuid: string
|
||||
senderUuid: string
|
||||
senderEncryptionKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
}): Promise<ClientDisplayableError[]> {
|
||||
const errors: ClientDisplayableError[] = []
|
||||
|
||||
const getUsersUseCase = new GetSharedVaultUsersUseCase(this.vaultUsersServer)
|
||||
const users = await getUsersUseCase.execute({ sharedVaultUuid: params.sharedVaultUuid })
|
||||
if (!users) {
|
||||
return [ClientDisplayableError.FromString('Cannot send root key changed message; users not found')]
|
||||
}
|
||||
|
||||
for (const user of users) {
|
||||
if (user.user_uuid === params.senderUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
const trustedContact = this.contacts.findTrustedContact(user.user_uuid)
|
||||
if (!trustedContact) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sendMessageResult = await this.sendToContact({
|
||||
keySystemIdentifier: params.keySystemIdentifier,
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
senderKeyPair: params.senderEncryptionKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
contact: trustedContact,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(sendMessageResult)) {
|
||||
errors.push(sendMessageResult)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private async sendToContact(params: {
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
sharedVaultUuid: string
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
contact: TrustedContactInterface
|
||||
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
|
||||
const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.keySystemIdentifier)
|
||||
if (!keySystemRootKey) {
|
||||
throw new Error(`Vault key not found for keySystemIdentifier ${params.keySystemIdentifier}`)
|
||||
}
|
||||
|
||||
const message: AsymmetricMessageSharedVaultRootKeyChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
|
||||
data: { recipientUuid: params.contact.contactUuid, rootKey: keySystemRootKey.content },
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
|
||||
message: message,
|
||||
senderKeyPair: params.senderKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
recipientPublicKey: params.contact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
const replaceabilityIdentifier = [
|
||||
AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
|
||||
params.sharedVaultUuid,
|
||||
params.keySystemIdentifier,
|
||||
].join(':')
|
||||
|
||||
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
|
||||
const sendMessageResult = await sendMessageUseCase.execute({
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
encryptedMessage,
|
||||
replaceabilityIdentifier,
|
||||
})
|
||||
|
||||
return sendMessageResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSharedVaultMetadataChanged,
|
||||
SharedVaultListingInterface,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { GetVaultUsers } from './GetVaultUsers'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
|
||||
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
|
||||
export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private encryptMessage: EncryptMessage,
|
||||
private findContact: FindContact,
|
||||
private getVaultUsers: GetVaultUsers,
|
||||
private sendMessage: SendMessage,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
vault: SharedVaultListingInterface
|
||||
senderUuid: string
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}): Promise<Result<void>> {
|
||||
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.vault.sharing.sharedVaultUuid })
|
||||
if (!users) {
|
||||
return Result.fail('Cannot send metadata changed message; users not found')
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
for (const user of users) {
|
||||
if (user.user_uuid === params.senderUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
const trustedContact = this.findContact.execute({ userUuid: user.user_uuid })
|
||||
if (trustedContact.isFailed()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sendMessageResult = await this.sendToContact({
|
||||
vault: params.vault,
|
||||
keys: params.keys,
|
||||
contact: trustedContact.getValue(),
|
||||
})
|
||||
|
||||
if (sendMessageResult.isFailed()) {
|
||||
errors.push(sendMessageResult.getError())
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(errors.join(', '))
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async sendToContact(params: {
|
||||
vault: SharedVaultListingInterface
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
contact: TrustedContactInterface
|
||||
}): Promise<Result<AsymmetricMessageServerHash>> {
|
||||
const message: AsymmetricMessageSharedVaultMetadataChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
|
||||
data: {
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
sharedVaultUuid: params.vault.sharing.sharedVaultUuid,
|
||||
name: params.vault.name,
|
||||
description: params.vault.description,
|
||||
},
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryptMessage.execute({
|
||||
message: message,
|
||||
keys: params.keys,
|
||||
recipientPublicKey: params.contact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (encryptedMessage.isFailed()) {
|
||||
return Result.fail(encryptedMessage.getError())
|
||||
}
|
||||
|
||||
const replaceabilityIdentifier = GetReplaceabilityIdentifier(
|
||||
AsymmetricMessagePayloadType.SharedVaultMetadataChanged,
|
||||
params.vault.sharing.sharedVaultUuid,
|
||||
params.vault.systemIdentifier,
|
||||
)
|
||||
|
||||
const sendMessageResult = await this.sendMessage.execute({
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
encryptedMessage: encryptedMessage.getValue(),
|
||||
replaceabilityIdentifier,
|
||||
})
|
||||
|
||||
return sendMessageResult
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
isErrorResponse,
|
||||
SharedVaultPermission,
|
||||
getErrorFromErrorResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
export class SendSharedVaultInviteUseCase {
|
||||
export class SendVaultInvite implements UseCaseInterface<SharedVaultInviteServerHash> {
|
||||
constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {}
|
||||
|
||||
async execute(params: {
|
||||
@@ -14,7 +15,7 @@ export class SendSharedVaultInviteUseCase {
|
||||
recipientUuid: string
|
||||
encryptedMessage: string
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
}): Promise<Result<SharedVaultInviteServerHash>> {
|
||||
const response = await this.vaultInvitesServer.createInvite({
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
recipientUuid: params.recipientUuid,
|
||||
@@ -23,9 +24,9 @@ export class SendSharedVaultInviteUseCase {
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromNetworkError(response)
|
||||
return Result.fail(getErrorFromErrorResponse(response).message)
|
||||
}
|
||||
|
||||
return response.data.invite
|
||||
return Result.ok(response.data.invite)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSharedVaultRootKeyChanged,
|
||||
KeySystemIdentifier,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { GetVaultUsers } from './GetVaultUsers'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
|
||||
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { GetReplaceabilityIdentifier } from '../../AsymmetricMessage/UseCase/GetReplaceabilityIdentifier'
|
||||
import { FindContact } from '../../Contacts/UseCase/FindContact'
|
||||
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
|
||||
|
||||
export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private encryptMessage: EncryptMessage,
|
||||
private keyManager: KeySystemKeyManagerInterface,
|
||||
private findContact: FindContact,
|
||||
private sendMessage: SendMessage,
|
||||
private getVaultUsers: GetVaultUsers,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
sharedVaultUuid: string
|
||||
senderUuid: string
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
}): Promise<Result<void>> {
|
||||
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid })
|
||||
if (!users) {
|
||||
return Result.fail('Cannot send root key changed message; users not found')
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
|
||||
for (const user of users) {
|
||||
if (user.user_uuid === params.senderUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
const trustedContact = this.findContact.execute({ userUuid: user.user_uuid })
|
||||
if (trustedContact.isFailed()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.sendToContact({
|
||||
keySystemIdentifier: params.keySystemIdentifier,
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
keys: params.keys,
|
||||
contact: trustedContact.getValue(),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
errors.push(result.getError())
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(errors.join(', '))
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async sendToContact(params: {
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
sharedVaultUuid: string
|
||||
keys: {
|
||||
encryption: PkcKeyPair
|
||||
signing: PkcKeyPair
|
||||
}
|
||||
contact: TrustedContactInterface
|
||||
}): Promise<Result<AsymmetricMessageServerHash>> {
|
||||
const keySystemRootKey = this.keyManager.getPrimaryKeySystemRootKey(params.keySystemIdentifier)
|
||||
if (!keySystemRootKey) {
|
||||
throw new Error(`Vault key not found for keySystemIdentifier ${params.keySystemIdentifier}`)
|
||||
}
|
||||
|
||||
const message: AsymmetricMessageSharedVaultRootKeyChanged = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
|
||||
data: { recipientUuid: params.contact.contactUuid, rootKey: keySystemRootKey.content },
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryptMessage.execute({
|
||||
message: message,
|
||||
keys: params.keys,
|
||||
recipientPublicKey: params.contact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
if (encryptedMessage.isFailed()) {
|
||||
return Result.fail(encryptedMessage.getError())
|
||||
}
|
||||
|
||||
const replaceabilityIdentifier = GetReplaceabilityIdentifier(
|
||||
AsymmetricMessagePayloadType.SharedVaultRootKeyChanged,
|
||||
params.sharedVaultUuid,
|
||||
params.keySystemIdentifier,
|
||||
)
|
||||
|
||||
const sendMessageResult = await this.sendMessage.execute({
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
encryptedMessage: encryptedMessage.getValue(),
|
||||
replaceabilityIdentifier,
|
||||
})
|
||||
|
||||
return sendMessageResult
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import {
|
||||
TrustedContactInterface,
|
||||
SharedVaultListingInterface,
|
||||
AsymmetricMessagePayloadType,
|
||||
} from '@standardnotes/models'
|
||||
import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase'
|
||||
|
||||
export class ShareContactWithAllMembersOfSharedVaultUseCase {
|
||||
constructor(
|
||||
private contacts: ContactServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private sharedVaultUsersServer: SharedVaultUsersServerInterface,
|
||||
private messageServer: AsymmetricMessageServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
senderUserUuid: string
|
||||
sharedVault: SharedVaultListingInterface
|
||||
contactToShare: TrustedContactInterface
|
||||
}): Promise<void | ClientDisplayableError> {
|
||||
if (params.sharedVault.sharing.ownerUserUuid !== params.senderUserUuid) {
|
||||
return ClientDisplayableError.FromString('Cannot share contact; user is not the owner of the shared vault')
|
||||
}
|
||||
|
||||
const usersResponse = await this.sharedVaultUsersServer.getSharedVaultUsers({
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(usersResponse)) {
|
||||
return ClientDisplayableError.FromString('Cannot share contact; shared vault users not found')
|
||||
}
|
||||
|
||||
const users = usersResponse.data.users
|
||||
if (users.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const messageSendUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
|
||||
|
||||
for (const vaultUser of users) {
|
||||
if (vaultUser.user_uuid === params.senderUserUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (vaultUser.user_uuid === params.contactToShare.contactUuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
const vaultUserAsContact = this.contacts.findTrustedContact(vaultUser.user_uuid)
|
||||
if (!vaultUserAsContact) {
|
||||
continue
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
|
||||
message: {
|
||||
type: AsymmetricMessagePayloadType.ContactShare,
|
||||
data: { recipientUuid: vaultUserAsContact.contactUuid, trustedContact: params.contactToShare.content },
|
||||
},
|
||||
senderKeyPair: params.senderKeyPair,
|
||||
senderSigningKeyPair: params.senderSigningKeyPair,
|
||||
recipientPublicKey: vaultUserAsContact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
await messageSendUseCase.execute({
|
||||
recipientUuid: vaultUserAsContact.contactUuid,
|
||||
encryptedMessage,
|
||||
replaceabilityIdentifier: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user