internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -18,8 +18,18 @@ export abstract class AlertService {
|
||||
cancelButtonText?: string,
|
||||
): Promise<boolean>
|
||||
|
||||
abstract confirmV2(dto: {
|
||||
text: string
|
||||
title?: string
|
||||
confirmButtonText?: string
|
||||
confirmButtonType?: ButtonType
|
||||
cancelButtonText?: string
|
||||
}): Promise<boolean>
|
||||
|
||||
abstract alert(text: string, title?: string, closeButtonText?: string): Promise<void>
|
||||
|
||||
abstract alertV2(dto: { text: string; title?: string; closeButtonText?: string }): Promise<void>
|
||||
|
||||
abstract blockingDialog(text: string, title?: string): DismissBlockingDialog | Promise<DismissBlockingDialog>
|
||||
|
||||
showErrorAlert(error: ClientDisplayableError): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { SyncOptions } from './../Sync/SyncOptions'
|
||||
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
|
||||
import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface'
|
||||
import { VaultServiceInterface } from './../Vaults/VaultServiceInterface'
|
||||
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
||||
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
||||
import {
|
||||
BackupFile,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
ItemStream,
|
||||
PayloadEmitSource,
|
||||
Platform,
|
||||
PrefKey,
|
||||
PrefValue,
|
||||
} from '@standardnotes/models'
|
||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||
|
||||
import { AlertService } from '../Alert/AlertService'
|
||||
@@ -9,7 +22,7 @@ import { ApplicationEventCallback } from '../Event/ApplicationEventCallback'
|
||||
import { FeaturesClientInterface } from '../Feature/FeaturesClientInterface'
|
||||
import { SubscriptionClientInterface } from '../Subscription/SubscriptionClientInterface'
|
||||
import { DeviceInterface } from '../Device/DeviceInterface'
|
||||
import { ItemsClientInterface } from '../Item/ItemsClientInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
|
||||
@@ -24,6 +37,7 @@ export interface ApplicationInterface {
|
||||
isStarted(): boolean
|
||||
isLaunched(): boolean
|
||||
addEventObserver(callback: ApplicationEventCallback, singleEvent?: ApplicationEvent): () => void
|
||||
addSingleEventObserver(event: ApplicationEvent, callback: ApplicationEventCallback): () => void
|
||||
hasProtectionSources(): boolean
|
||||
createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined>
|
||||
createEncryptedBackupFile(): Promise<BackupFile | undefined>
|
||||
@@ -32,7 +46,7 @@ export interface ApplicationInterface {
|
||||
lock(): Promise<void>
|
||||
softLockBiometrics(): void
|
||||
setValue(key: string, value: unknown, mode?: StorageValueModes): void
|
||||
getValue(key: string, mode?: StorageValueModes): unknown
|
||||
getValue<T>(key: string, mode?: StorageValueModes): T
|
||||
removeValue(key: string, mode?: StorageValueModes): Promise<void>
|
||||
isLocked(): Promise<boolean>
|
||||
getPreference<K extends PrefKey>(key: K): PrefValue[K] | undefined
|
||||
@@ -44,15 +58,42 @@ export interface ApplicationInterface {
|
||||
stream: ItemStream<I>,
|
||||
): () => void
|
||||
hasAccount(): boolean
|
||||
|
||||
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
|
||||
/**
|
||||
* Mutates a pre-existing item, marks it as dirty, and syncs it
|
||||
*/
|
||||
changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
|
||||
/**
|
||||
* Mutates pre-existing items, marks them as dirty, and syncs
|
||||
*/
|
||||
changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<void>
|
||||
|
||||
get features(): FeaturesClientInterface
|
||||
get componentManager(): ComponentManagerInterface
|
||||
get items(): ItemsClientInterface
|
||||
get items(): ItemManagerInterface
|
||||
get mutator(): MutatorClientInterface
|
||||
get user(): UserClientInterface
|
||||
get files(): FilesClientInterface
|
||||
get subscriptions(): SubscriptionClientInterface
|
||||
get fileBackups(): BackupServiceInterface | undefined
|
||||
get sessions(): SessionsClientInterface
|
||||
get vaults(): VaultServiceInterface
|
||||
get challenges(): ChallengeServiceInterface
|
||||
get alerts(): AlertService
|
||||
readonly identifier: ApplicationIdentifier
|
||||
readonly platform: Platform
|
||||
deviceInterface: DeviceInterface
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { MobileDeviceInterface } from './../Device/MobileDeviceInterface'
|
||||
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
||||
import { WebAppEvent } from '../Event/WebAppEvent'
|
||||
import { ApplicationInterface } from './ApplicationInterface'
|
||||
|
||||
export interface WebApplicationInterface extends ApplicationInterface {
|
||||
notifyWebEvent(event: WebAppEvent, data?: unknown): void
|
||||
getDesktopService(): DesktopManagerInterface | undefined
|
||||
handleMobileEnteringBackgroundEvent(): Promise<void>
|
||||
handleMobileGainingFocusEvent(): Promise<void>
|
||||
handleMobileLosingFocusEvent(): Promise<void>
|
||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||
handleMobileColorSchemeChangeEvent(): void
|
||||
handleMobileKeyboardWillChangeFrameEvent(frame: { height: number; contentHeight: number }): void
|
||||
handleMobileKeyboardDidChangeFrameEvent(frame: { height: number; contentHeight: number }): void
|
||||
isNativeMobileWeb(): boolean
|
||||
mobileDevice(): MobileDeviceInterface
|
||||
handleAndroidBackButtonPressed(): void
|
||||
addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined
|
||||
setAndroidBackHandlerFallbackListener(listener: () => boolean): void
|
||||
generateUUID(): string
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { HttpServiceInterface } from '@standardnotes/api'
|
||||
import { AsymmetricMessageService } from './AsymmetricMessageService'
|
||||
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessagePayloadType } from '@standardnotes/models'
|
||||
|
||||
describe('AsymmetricMessageService', () => {
|
||||
let service: AsymmetricMessageService
|
||||
|
||||
beforeEach(() => {
|
||||
const http = {} as jest.Mocked<HttpServiceInterface>
|
||||
http.delete = jest.fn()
|
||||
|
||||
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
|
||||
const contacts = {} as jest.Mocked<ContactServiceInterface>
|
||||
const items = {} as jest.Mocked<ItemManagerInterface>
|
||||
const sync = {} as jest.Mocked<SyncServiceInterface>
|
||||
const mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
|
||||
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
eventBus.addEventHandler = jest.fn()
|
||||
|
||||
service = new AsymmetricMessageService(http, encryption, contacts, items, mutator, sync, eventBus)
|
||||
})
|
||||
|
||||
it('should process incoming messages oldest first', async () => {
|
||||
const messages: AsymmetricMessageServerHash[] = [
|
||||
{
|
||||
uuid: 'newer-message',
|
||||
user_uuid: '1',
|
||||
sender_uuid: '2',
|
||||
encrypted_message: 'encrypted_message',
|
||||
created_at_timestamp: 2,
|
||||
updated_at_timestamp: 2,
|
||||
},
|
||||
{
|
||||
uuid: 'older-message',
|
||||
user_uuid: '1',
|
||||
sender_uuid: '2',
|
||||
encrypted_message: 'encrypted_message',
|
||||
created_at_timestamp: 1,
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } }
|
||||
|
||||
service.getTrustedMessagePayload = jest.fn().mockReturnValue(trustedPayloadMock)
|
||||
|
||||
const handleTrustedContactShareMessageMock = jest.fn()
|
||||
service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock
|
||||
|
||||
await service.handleRemoteReceivedAsymmetricMessages(messages)
|
||||
|
||||
expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1])
|
||||
expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { ContactServiceInterface } from './../Contacts/ContactServiceInterface'
|
||||
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { GetAsymmetricMessageTrustedPayload } from './UseCase/GetAsymmetricMessageTrustedPayload'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import {
|
||||
AsymmetricMessageSharedVaultRootKeyChanged,
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSenderKeypairChanged,
|
||||
AsymmetricMessageTrustedContactShare,
|
||||
AsymmetricMessagePayload,
|
||||
AsymmetricMessageSharedVaultMetadataChanged,
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { HandleTrustedSharedVaultRootKeyChangedMessage } from './UseCase/HandleTrustedSharedVaultRootKeyChangedMessage'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { SessionEvent } from '../Session/SessionEvent'
|
||||
import { AsymmetricMessageServer, HttpServiceInterface } from '@standardnotes/api'
|
||||
import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData'
|
||||
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
|
||||
import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages'
|
||||
import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages'
|
||||
import { GetVaultUseCase } from '../Vaults/UseCase/GetVault'
|
||||
|
||||
export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface {
|
||||
private messageServer: AsymmetricMessageServer
|
||||
|
||||
constructor(
|
||||
http: HttpServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
this.messageServer = new AsymmetricMessageServer(http)
|
||||
|
||||
eventBus.addEventHandler(this, SyncEvent.ReceivedAsymmetricMessages)
|
||||
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SessionEvent.UserKeyPairChanged) {
|
||||
void this.messageServer.deleteAllInboundMessages()
|
||||
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
|
||||
}
|
||||
|
||||
if (event.type === SyncEvent.ReceivedAsymmetricMessages) {
|
||||
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
|
||||
}
|
||||
}
|
||||
|
||||
public async getOutboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
|
||||
const usecase = new GetOutboundAsymmetricMessages(this.messageServer)
|
||||
return usecase.execute()
|
||||
}
|
||||
|
||||
public async getInboundMessages(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
|
||||
const usecase = new GetInboundAsymmetricMessages(this.messageServer)
|
||||
return usecase.execute()
|
||||
}
|
||||
|
||||
async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
|
||||
if (!data.oldKeyPair || !data.oldSigningKeyPair) {
|
||||
return
|
||||
}
|
||||
|
||||
const useCase = new SendOwnContactChangeMessage(this.encryption, this.messageServer)
|
||||
|
||||
const contacts = this.contacts.getAllContacts()
|
||||
|
||||
for (const contact of contacts) {
|
||||
if (contact.isMe) {
|
||||
continue
|
||||
}
|
||||
|
||||
await useCase.execute({
|
||||
senderOldKeyPair: data.oldKeyPair,
|
||||
senderOldSigningKeyPair: data.oldSigningKeyPair,
|
||||
senderNewKeyPair: data.newKeyPair,
|
||||
senderNewSigningKeyPair: data.newSigningKeyPair,
|
||||
contact,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise<void> {
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const sortedMessages = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp)
|
||||
|
||||
for (const message of sortedMessages) {
|
||||
const trustedMessagePayload = this.getTrustedMessagePayload(message)
|
||||
if (!trustedMessagePayload) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (trustedMessagePayload.data.recipientUuid !== message.user_uuid) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (trustedMessagePayload.type === AsymmetricMessagePayloadType.ContactShare) {
|
||||
await this.handleTrustedContactShareMessage(message, trustedMessagePayload)
|
||||
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
|
||||
await this.handleTrustedSenderKeypairChangedMessage(message, trustedMessagePayload)
|
||||
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) {
|
||||
await this.handleTrustedSharedVaultRootKeyChangedMessage(message, trustedMessagePayload)
|
||||
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) {
|
||||
await this.handleVaultMetadataChangedMessage(message, trustedMessagePayload)
|
||||
} else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultInvite) {
|
||||
throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages')
|
||||
}
|
||||
|
||||
await this.deleteMessageAfterProcessing(message)
|
||||
}
|
||||
}
|
||||
|
||||
getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined {
|
||||
const useCase = new GetAsymmetricMessageTrustedPayload(this.encryption, this.contacts)
|
||||
|
||||
return useCase.execute({
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
private async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise<void> {
|
||||
await this.messageServer.deleteMessage({ messageUuid: message.uuid })
|
||||
}
|
||||
|
||||
async handleVaultMetadataChangedMessage(
|
||||
_message: AsymmetricMessageServerHash,
|
||||
trustedPayload: AsymmetricMessageSharedVaultMetadataChanged,
|
||||
): Promise<void> {
|
||||
const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: trustedPayload.data.sharedVaultUuid })
|
||||
if (!vault) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.name = trustedPayload.data.name
|
||||
mutator.description = trustedPayload.data.description
|
||||
})
|
||||
}
|
||||
|
||||
async handleTrustedContactShareMessage(
|
||||
_message: AsymmetricMessageServerHash,
|
||||
trustedPayload: AsymmetricMessageTrustedContactShare,
|
||||
): Promise<void> {
|
||||
await this.contacts.createOrUpdateTrustedContactFromContactShare(trustedPayload.data.trustedContact)
|
||||
}
|
||||
|
||||
private async handleTrustedSenderKeypairChangedMessage(
|
||||
message: AsymmetricMessageServerHash,
|
||||
trustedPayload: AsymmetricMessageSenderKeypairChanged,
|
||||
): Promise<void> {
|
||||
await this.contacts.createOrEditTrustedContact({
|
||||
contactUuid: message.sender_uuid,
|
||||
publicKey: trustedPayload.data.newEncryptionPublicKey,
|
||||
signingPublicKey: trustedPayload.data.newSigningPublicKey,
|
||||
})
|
||||
}
|
||||
|
||||
private async handleTrustedSharedVaultRootKeyChangedMessage(
|
||||
_message: AsymmetricMessageServerHash,
|
||||
trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged,
|
||||
): Promise<void> {
|
||||
const useCase = new HandleTrustedSharedVaultRootKeyChangedMessage(
|
||||
this.mutator,
|
||||
this.items,
|
||||
this.sync,
|
||||
this.encryption,
|
||||
)
|
||||
await useCase.execute(trustedPayload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessagePayload } from '@standardnotes/models'
|
||||
|
||||
export class GetAsymmetricMessageTrustedPayload<M extends AsymmetricMessagePayload> {
|
||||
constructor(private encryption: EncryptionProviderInterface, private contacts: ContactServiceInterface) {}
|
||||
|
||||
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
|
||||
const trustedContact = this.contacts.findTrustedContact(dto.message.sender_uuid)
|
||||
if (!trustedContact) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
|
||||
encryptedString: dto.message.encrypted_message,
|
||||
trustedSender: trustedContact,
|
||||
privateKey: dto.privateKey,
|
||||
})
|
||||
|
||||
return decryptionResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessagePayload } from '@standardnotes/models'
|
||||
|
||||
export class GetAsymmetricMessageUntrustedPayload<M extends AsymmetricMessagePayload> {
|
||||
constructor(private encryption: EncryptionProviderInterface) {}
|
||||
|
||||
execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined {
|
||||
const decryptionResult = this.encryption.asymmetricallyDecryptMessage<M>({
|
||||
encryptedString: dto.message.encrypted_message,
|
||||
trustedSender: undefined,
|
||||
privateKey: dto.privateKey,
|
||||
})
|
||||
|
||||
return decryptionResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class GetInboundAsymmetricMessages {
|
||||
constructor(private messageServer: AsymmetricMessageServerInterface) {}
|
||||
|
||||
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
|
||||
const response = await this.messageServer.getMessages()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromError(response.data.error)
|
||||
}
|
||||
|
||||
return response.data.messages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class GetOutboundAsymmetricMessages {
|
||||
constructor(private messageServer: AsymmetricMessageServerInterface) {}
|
||||
|
||||
async execute(): Promise<AsymmetricMessageServerHash[] | ClientDisplayableError> {
|
||||
const response = await this.messageServer.getOutboundUserMessages()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromError(response.data.error)
|
||||
}
|
||||
|
||||
return response.data.messages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { HandleTrustedSharedVaultInviteMessage } from './HandleTrustedSharedVaultInviteMessage'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSharedVaultInvite,
|
||||
KeySystemRootKeyContent,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
describe('HandleTrustedSharedVaultInviteMessage', () => {
|
||||
let mutatorMock: jest.Mocked<MutatorClientInterface>
|
||||
let syncServiceMock: jest.Mocked<SyncServiceInterface>
|
||||
let contactServiceMock: jest.Mocked<ContactServiceInterface>
|
||||
|
||||
beforeEach(() => {
|
||||
mutatorMock = {
|
||||
createItem: jest.fn(),
|
||||
} as any
|
||||
|
||||
syncServiceMock = {
|
||||
sync: jest.fn(),
|
||||
} as any
|
||||
|
||||
contactServiceMock = {
|
||||
createOrEditTrustedContact: jest.fn(),
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should create root key before creating vault listing so that propagated vault listings do not appear as locked', async () => {
|
||||
const handleTrustedSharedVaultInviteMessage = new HandleTrustedSharedVaultInviteMessage(
|
||||
mutatorMock,
|
||||
syncServiceMock,
|
||||
contactServiceMock,
|
||||
)
|
||||
|
||||
const testMessage = {
|
||||
type: AsymmetricMessagePayloadType.SharedVaultInvite,
|
||||
data: {
|
||||
recipientUuid: 'test-recipient-uuid',
|
||||
rootKey: {
|
||||
systemIdentifier: 'test-system-identifier',
|
||||
} as jest.Mocked<KeySystemRootKeyContent>,
|
||||
metadata: {
|
||||
name: 'test-name',
|
||||
},
|
||||
trustedContacts: [],
|
||||
},
|
||||
} as jest.Mocked<AsymmetricMessageSharedVaultInvite>
|
||||
|
||||
const sharedVaultUuid = 'test-shared-vault-uuid'
|
||||
const senderUuid = 'test-sender-uuid'
|
||||
|
||||
await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid)
|
||||
|
||||
const keySystemRootKeyCallIndex = mutatorMock.createItem.mock.calls.findIndex(
|
||||
([contentType]) => contentType === ContentType.KeySystemRootKey,
|
||||
)
|
||||
|
||||
const vaultListingCallIndex = mutatorMock.createItem.mock.calls.findIndex(
|
||||
([contentType]) => contentType === ContentType.VaultListing,
|
||||
)
|
||||
|
||||
expect(keySystemRootKeyCallIndex).toBeLessThan(vaultListingCallIndex)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ContactServiceInterface } from './../../Contacts/ContactServiceInterface'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import {
|
||||
KeySystemRootKeyInterface,
|
||||
AsymmetricMessageSharedVaultInvite,
|
||||
KeySystemRootKeyContent,
|
||||
FillItemContent,
|
||||
FillItemContentSpecialized,
|
||||
VaultListingContentSpecialized,
|
||||
KeySystemRootKeyStorageMode,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class HandleTrustedSharedVaultInviteMessage {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private contacts: ContactServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
message: AsymmetricMessageSharedVaultInvite,
|
||||
sharedVaultUuid: string,
|
||||
senderUuid: string,
|
||||
): Promise<void> {
|
||||
const { rootKey: rootKeyContent, trustedContacts, metadata } = message.data
|
||||
|
||||
const content: VaultListingContentSpecialized = {
|
||||
systemIdentifier: rootKeyContent.systemIdentifier,
|
||||
rootKeyParams: rootKeyContent.keyParams,
|
||||
keyStorageMode: KeySystemRootKeyStorageMode.Synced,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
sharing: {
|
||||
sharedVaultUuid: sharedVaultUuid,
|
||||
ownerUserUuid: senderUuid,
|
||||
},
|
||||
}
|
||||
|
||||
await this.mutator.createItem<KeySystemRootKeyInterface>(
|
||||
ContentType.KeySystemRootKey,
|
||||
FillItemContent<KeySystemRootKeyContent>(rootKeyContent),
|
||||
true,
|
||||
)
|
||||
|
||||
await this.mutator.createItem(ContentType.VaultListing, FillItemContentSpecialized(content), true)
|
||||
|
||||
for (const contact of trustedContacts) {
|
||||
if (contact.isMe) {
|
||||
throw new Error('Should not receive isMe contact from invite')
|
||||
}
|
||||
|
||||
await this.contacts.createOrEditTrustedContact({
|
||||
name: contact.name,
|
||||
contactUuid: contact.contactUuid,
|
||||
publicKey: contact.publicKeySet.encryption,
|
||||
signingPublicKey: contact.publicKeySet.signing,
|
||||
})
|
||||
}
|
||||
|
||||
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { SyncServiceInterface } from '../../Sync/SyncServiceInterface'
|
||||
import {
|
||||
KeySystemRootKeyInterface,
|
||||
AsymmetricMessageSharedVaultRootKeyChanged,
|
||||
FillItemContent,
|
||||
KeySystemRootKeyContent,
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { GetVaultUseCase } from '../../Vaults/UseCase/GetVault'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
|
||||
export class HandleTrustedSharedVaultRootKeyChangedMessage {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
) {}
|
||||
|
||||
async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise<void> {
|
||||
const rootKeyContent = message.data.rootKey
|
||||
|
||||
await this.mutator.createItem<KeySystemRootKeyInterface>(
|
||||
ContentType.KeySystemRootKey,
|
||||
FillItemContent<KeySystemRootKeyContent>(rootKeyContent),
|
||||
true,
|
||||
)
|
||||
|
||||
const vault = new GetVaultUseCase(this.items).execute({ keySystemIdentifier: rootKeyContent.systemIdentifier })
|
||||
if (vault) {
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.rootKeyParams = rootKeyContent.keyParams
|
||||
})
|
||||
}
|
||||
|
||||
await this.encryption.decryptErroredPayloads()
|
||||
|
||||
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses'
|
||||
import { AsymmetricMessageServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class SendAsymmetricMessageUseCase {
|
||||
constructor(private messageServer: AsymmetricMessageServerInterface) {}
|
||||
|
||||
async execute(params: {
|
||||
recipientUuid: string
|
||||
encryptedMessage: string
|
||||
replaceabilityIdentifier: string | undefined
|
||||
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
|
||||
const response = await this.messageServer.createMessage({
|
||||
recipientUuid: params.recipientUuid,
|
||||
encryptedMessage: params.encryptedMessage,
|
||||
replaceabilityIdentifier: params.replaceabilityIdentifier,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromError(response.data.error)
|
||||
}
|
||||
|
||||
return response.data.message
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses'
|
||||
import {
|
||||
TrustedContactInterface,
|
||||
AsymmetricMessagePayloadType,
|
||||
AsymmetricMessageSenderKeypairChanged,
|
||||
} from '@standardnotes/models'
|
||||
import { AsymmetricMessageServer } from '@standardnotes/api'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
import { SendAsymmetricMessageUseCase } from './SendAsymmetricMessageUseCase'
|
||||
|
||||
export class SendOwnContactChangeMessage {
|
||||
constructor(private encryption: EncryptionProviderInterface, private messageServer: AsymmetricMessageServer) {}
|
||||
|
||||
async execute(params: {
|
||||
senderOldKeyPair: PkcKeyPair
|
||||
senderOldSigningKeyPair: PkcKeyPair
|
||||
senderNewKeyPair: PkcKeyPair
|
||||
senderNewSigningKeyPair: PkcKeyPair
|
||||
contact: TrustedContactInterface
|
||||
}): Promise<AsymmetricMessageServerHash | ClientDisplayableError> {
|
||||
const message: AsymmetricMessageSenderKeypairChanged = {
|
||||
type: AsymmetricMessagePayloadType.SenderKeypairChanged,
|
||||
data: {
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
newEncryptionPublicKey: params.senderNewKeyPair.publicKey,
|
||||
newSigningPublicKey: params.senderNewSigningKeyPair.publicKey,
|
||||
},
|
||||
}
|
||||
|
||||
const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({
|
||||
message: message,
|
||||
senderKeyPair: params.senderOldKeyPair,
|
||||
senderSigningKeyPair: params.senderOldSigningKeyPair,
|
||||
recipientPublicKey: params.contact.publicKeySet.encryption,
|
||||
})
|
||||
|
||||
const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer)
|
||||
const sendMessageResult = await sendMessageUseCase.execute({
|
||||
recipientUuid: params.contact.contactUuid,
|
||||
encryptedMessage,
|
||||
replaceabilityIdentifier: undefined,
|
||||
})
|
||||
|
||||
return sendMessageResult
|
||||
}
|
||||
}
|
||||
@@ -32,16 +32,13 @@ describe('backup service', () => {
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<ApiServiceInterface>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
apiService.createFileValetToken = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
apiService.downloadFile = jest.fn()
|
||||
apiService.deleteFile = jest.fn().mockReturnValue({})
|
||||
|
||||
itemManager = {} as jest.Mocked<ItemManagerInterface>
|
||||
itemManager.createItem = jest.fn()
|
||||
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
|
||||
itemManager.setItemToBeDeleted = jest.fn()
|
||||
itemManager.addObserver = jest.fn()
|
||||
itemManager.changeItem = jest.fn()
|
||||
|
||||
status = {} as jest.Mocked<StatusServiceInterface>
|
||||
|
||||
|
||||
@@ -515,7 +515,7 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
},
|
||||
})
|
||||
|
||||
const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read')
|
||||
const token = await this.api.createUserFileValetToken(file.remoteIdentifier, 'read')
|
||||
|
||||
if (token instanceof ClientDisplayableError) {
|
||||
this.status.removeMessage(messageId)
|
||||
@@ -536,9 +536,11 @@ export class FilesBackupService extends AbstractService implements BackupService
|
||||
|
||||
const metaFileAsString = JSON.stringify(metaFile, null, 2)
|
||||
|
||||
const downloadType = !file.user_uuid || file.user_uuid === this.session.getSureUser().uuid ? 'user' : 'shared-vault'
|
||||
|
||||
const result = await this.device.saveFilesBackupsFile(location, file.uuid, metaFileAsString, {
|
||||
chunkSizes: file.encryptedChunkSizes,
|
||||
url: this.api.getFilesDownloadUrl(),
|
||||
url: this.api.getFilesDownloadUrl(downloadType),
|
||||
valetToken: token,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ChallengeArtifacts } from './Types/ChallengeArtifacts'
|
||||
import { ChallengeValue } from './Types/ChallengeValue'
|
||||
import { RootKeyInterface } from '@standardnotes/models'
|
||||
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
@@ -5,6 +7,7 @@ import { ChallengeInterface } from './ChallengeInterface'
|
||||
import { ChallengePromptInterface } from './Prompt/ChallengePromptInterface'
|
||||
import { ChallengeResponseInterface } from './ChallengeResponseInterface'
|
||||
import { ChallengeReason } from './Types/ChallengeReason'
|
||||
import { ChallengeObserver } from './Types/ChallengeObserver'
|
||||
|
||||
export interface ChallengeServiceInterface extends AbstractService {
|
||||
/**
|
||||
@@ -20,7 +23,7 @@ export interface ChallengeServiceInterface extends AbstractService {
|
||||
subheading?: string,
|
||||
): ChallengeInterface
|
||||
completeChallenge(challenge: ChallengeInterface): void
|
||||
promptForAccountPassword(): Promise<boolean>
|
||||
promptForAccountPassword(): Promise<string | null>
|
||||
getWrappingKeyIfApplicable(passcode?: string): Promise<
|
||||
| {
|
||||
canceled?: undefined
|
||||
@@ -35,4 +38,11 @@ export interface ChallengeServiceInterface extends AbstractService {
|
||||
canceled?: undefined
|
||||
}
|
||||
>
|
||||
addChallengeObserver(challenge: ChallengeInterface, observer: ChallengeObserver): () => void
|
||||
setValidationStatusForChallenge(
|
||||
challenge: ChallengeInterface,
|
||||
value: ChallengeValue,
|
||||
valid: boolean,
|
||||
artifacts?: ChallengeArtifacts,
|
||||
): void
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ChallengeResponseInterface } from '../ChallengeResponseInterface'
|
||||
import { ChallengeValueCallback } from './ChallengeValueCallback'
|
||||
|
||||
export type ChallengeObserver = {
|
||||
onValidValue?: ChallengeValueCallback
|
||||
onInvalidValue?: ChallengeValueCallback
|
||||
onNonvalidatedSubmit?: (response: ChallengeResponseInterface) => void
|
||||
onComplete?: (response: ChallengeResponseInterface) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ChallengeValue } from './ChallengeValue'
|
||||
|
||||
export type ChallengeValueCallback = (value: ChallengeValue) => void
|
||||
@@ -11,3 +11,5 @@ export * from './Types/ChallengeRawValue'
|
||||
export * from './Types/ChallengeReason'
|
||||
export * from './Types/ChallengeValidation'
|
||||
export * from './Types/ChallengeValue'
|
||||
export * from './Types/ChallengeObserver'
|
||||
export * from './Types/ChallengeValueCallback'
|
||||
|
||||
@@ -21,4 +21,6 @@ export interface ComponentManagerInterface {
|
||||
presentPermissionsDialog(_dialog: PermissionDialog): void
|
||||
legacyGetDefaultEditor(): SNComponent | undefined
|
||||
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined
|
||||
toggleTheme(uuid: string): Promise<void>
|
||||
toggleComponent(uuid: string): Promise<void>
|
||||
}
|
||||
|
||||
8
packages/services/src/Domain/Contacts/CollaborationID.ts
Normal file
8
packages/services/src/Domain/Contacts/CollaborationID.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const Version1CollaborationId = '1'
|
||||
|
||||
export type CollaborationIDData = {
|
||||
version: string
|
||||
userUuid: string
|
||||
publicKey: string
|
||||
signingPublicKey: string
|
||||
}
|
||||
264
packages/services/src/Domain/Contacts/ContactService.ts
Normal file
264
packages/services/src/Domain/Contacts/ContactService.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
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 { ContentType } from '@standardnotes/common'
|
||||
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'
|
||||
|
||||
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 encryption: EncryptionProviderInterface,
|
||||
singletons: SingletonManagerInterface,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private get userUuid(): string {
|
||||
return this.session.getSureUser().uuid
|
||||
}
|
||||
|
||||
getSelfContact(): TrustedContactInterface | undefined {
|
||||
return this.selfContactManager.selfContact
|
||||
}
|
||||
|
||||
public isCollaborationEnabled(): boolean {
|
||||
return !this.session.isUserMissingKeyPair()
|
||||
}
|
||||
|
||||
public async enableCollaboration(): Promise<void> {
|
||||
await this.user.updateAccountWithFirstTimeKeyPair()
|
||||
}
|
||||
|
||||
public getCollaborationID(): string {
|
||||
const publicKey = this.session.getPublicKey()
|
||||
if (!publicKey) {
|
||||
throw new Error('Collaboration not enabled')
|
||||
}
|
||||
|
||||
return this.buildCollaborationId({
|
||||
version: Version1CollaborationId,
|
||||
userUuid: this.session.getSureUser().uuid,
|
||||
publicKey,
|
||||
signingPublicKey: this.session.getSigningPublicKey(),
|
||||
})
|
||||
}
|
||||
|
||||
private buildCollaborationId(params: CollaborationIDData): string {
|
||||
const string = `${params.version}:${params.userUuid}:${params.publicKey}:${params.signingPublicKey}`
|
||||
return this.crypto.base64Encode(string)
|
||||
}
|
||||
|
||||
public parseCollaborationID(collaborationID: string): CollaborationIDData {
|
||||
const decoded = this.crypto.base64Decode(collaborationID)
|
||||
const [version, userUuid, publicKey, signingPublicKey] = decoded.split(':')
|
||||
return { version, userUuid, publicKey, signingPublicKey }
|
||||
}
|
||||
|
||||
public getCollaborationIDFromInvite(invite: SharedVaultInviteServerHash): string {
|
||||
const publicKeySet = this.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString(
|
||||
invite.encrypted_message,
|
||||
)
|
||||
return this.buildCollaborationId({
|
||||
version: Version1CollaborationId,
|
||||
userUuid: invite.sender_uuid,
|
||||
publicKey: publicKeySet.encryption,
|
||||
signingPublicKey: publicKeySet.signing,
|
||||
})
|
||||
}
|
||||
|
||||
public addTrustedContactFromCollaborationID(
|
||||
collaborationID: string,
|
||||
name?: string,
|
||||
): Promise<TrustedContactInterface | undefined> {
|
||||
const { userUuid, publicKey, signingPublicKey } = this.parseCollaborationID(collaborationID)
|
||||
return this.createOrEditTrustedContact({
|
||||
name: name ?? '',
|
||||
contactUuid: userUuid,
|
||||
publicKey,
|
||||
signingPublicKey,
|
||||
})
|
||||
}
|
||||
|
||||
async editTrustedContactFromCollaborationID(
|
||||
contact: TrustedContactInterface,
|
||||
params: { name: string; collaborationID: string },
|
||||
): Promise<TrustedContactInterface> {
|
||||
const { publicKey, signingPublicKey, userUuid } = this.parseCollaborationID(params.collaborationID)
|
||||
if (userUuid !== contact.contactUuid) {
|
||||
throw new Error("Collaboration ID's user uuid does not match contact UUID")
|
||||
}
|
||||
|
||||
const updatedContact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(
|
||||
contact,
|
||||
(mutator) => {
|
||||
mutator.name = params.name
|
||||
|
||||
if (publicKey !== contact.publicKeySet.encryption || signingPublicKey !== contact.publicKeySet.signing) {
|
||||
mutator.addPublicKey({
|
||||
encryption: publicKey,
|
||||
signing: signingPublicKey,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return updatedContact
|
||||
}
|
||||
|
||||
async updateTrustedContact(
|
||||
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)
|
||||
|
||||
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.TrustedContact,
|
||||
FillItemContent<TrustedContactContent>(data),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return contact
|
||||
}
|
||||
|
||||
async createOrEditTrustedContact(params: {
|
||||
name?: string
|
||||
contactUuid: string
|
||||
publicKey: string
|
||||
signingPublicKey: string
|
||||
isMe?: boolean
|
||||
}): Promise<TrustedContactInterface | undefined> {
|
||||
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync)
|
||||
const contact = await usecase.execute(params)
|
||||
return contact
|
||||
}
|
||||
|
||||
async deleteContact(contact: TrustedContactInterface): Promise<void> {
|
||||
if (contact.isMe) {
|
||||
throw new Error('Cannot delete self')
|
||||
}
|
||||
|
||||
await this.mutator.setItemToBeDeleted(contact)
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
getAllContacts(): TrustedContactInterface[] {
|
||||
return this.items.getItems(ContentType.TrustedContact)
|
||||
}
|
||||
|
||||
findTrustedContact(userUuid: string): TrustedContactInterface | undefined {
|
||||
const usecase = new FindTrustedContactUseCase(this.items)
|
||||
return usecase.execute({ userUuid })
|
||||
}
|
||||
|
||||
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
|
||||
return this.findTrustedContact(user.user_uuid)
|
||||
}
|
||||
|
||||
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
|
||||
return this.findTrustedContact(invite.user_uuid)
|
||||
}
|
||||
|
||||
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
|
||||
return this.buildCollaborationId({
|
||||
version: Version1CollaborationId,
|
||||
userUuid: contact.content.contactUuid,
|
||||
publicKey: contact.content.publicKeySet.encryption,
|
||||
signingPublicKey: contact.content.publicKeySet.signing,
|
||||
})
|
||||
}
|
||||
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
const usecase = new ValidateItemSignerUseCase(this.items)
|
||||
return usecase.execute(item)
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
this.selfContactManager.deinit()
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.selfContactManager as unknown) = undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
|
||||
import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult'
|
||||
|
||||
export enum ContactServiceEvent {}
|
||||
|
||||
export interface ContactServiceInterface extends AbstractService<ContactServiceEvent> {
|
||||
isCollaborationEnabled(): boolean
|
||||
enableCollaboration(): Promise<void>
|
||||
getCollaborationID(): string
|
||||
getCollaborationIDFromInvite(invite: SharedVaultInviteServerHash): string
|
||||
addTrustedContactFromCollaborationID(
|
||||
collaborationID: string,
|
||||
name?: string,
|
||||
): Promise<TrustedContactInterface | undefined>
|
||||
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string
|
||||
|
||||
createOrEditTrustedContact(params: {
|
||||
contactUuid: string
|
||||
name?: string
|
||||
publicKey: string
|
||||
signingPublicKey: string
|
||||
}): Promise<TrustedContactInterface | undefined>
|
||||
createOrUpdateTrustedContactFromContactShare(data: TrustedContactContentSpecialized): Promise<TrustedContactInterface>
|
||||
editTrustedContactFromCollaborationID(
|
||||
contact: TrustedContactInterface,
|
||||
params: { name: string; collaborationID: string },
|
||||
): Promise<TrustedContactInterface>
|
||||
deleteContact(contact: TrustedContactInterface): Promise<void>
|
||||
|
||||
getAllContacts(): TrustedContactInterface[]
|
||||
getSelfContact(): TrustedContactInterface | undefined
|
||||
findTrustedContact(userUuid: string): TrustedContactInterface | undefined
|
||||
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
|
||||
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
|
||||
|
||||
isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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 {
|
||||
ContactPublicKeySet,
|
||||
FillItemContent,
|
||||
TrustedContact,
|
||||
TrustedContactContent,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { CreateOrEditTrustedContactUseCase } from '../UseCase/CreateOrEditTrustedContact'
|
||||
import { PublicKeySet } from '@standardnotes/encryption'
|
||||
|
||||
export class SelfContactManager {
|
||||
public selfContact?: TrustedContactInterface
|
||||
private shouldReloadSelfContact = true
|
||||
private isReloadingSelfContact = false
|
||||
private eventDisposers: (() => void)[] = []
|
||||
|
||||
constructor(
|
||||
private sync: SyncServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private session: SessionsClientInterface,
|
||||
private singletons: SingletonManagerInterface,
|
||||
) {
|
||||
this.eventDisposers.push(
|
||||
items.addObserver(ContentType.TrustedContact, () => {
|
||||
this.shouldReloadSelfContact = true
|
||||
}),
|
||||
)
|
||||
|
||||
this.eventDisposers.push(
|
||||
sync.addEventObserver((event) => {
|
||||
if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) {
|
||||
void this.reloadSelfContact()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
public async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
if (stage === ApplicationStage.LoadedDatabase_12) {
|
||||
this.selfContact = this.singletons.findSingleton<TrustedContactInterface>(
|
||||
ContentType.UserPrefs,
|
||||
TrustedContact.singletonPredicate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) {
|
||||
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.selfContact) {
|
||||
return
|
||||
}
|
||||
|
||||
const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync)
|
||||
await usecase.execute({
|
||||
name: 'Me',
|
||||
contactUuid: this.selfContact.contactUuid,
|
||||
publicKey: publicKeySet.encryption,
|
||||
signingPublicKey: publicKeySet.signing,
|
||||
})
|
||||
}
|
||||
|
||||
private async reloadSelfContact() {
|
||||
if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.shouldReloadSelfContact || this.isReloadingSelfContact) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.session.isSignedIn()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.session.isUserMissingKeyPair()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isReloadingSelfContact = true
|
||||
|
||||
const content: TrustedContactContentSpecialized = {
|
||||
name: 'Me',
|
||||
isMe: true,
|
||||
contactUuid: this.session.getSureUser().uuid,
|
||||
publicKeySet: ContactPublicKeySet.FromJson({
|
||||
encryption: this.session.getPublicKey(),
|
||||
signing: this.session.getSigningPublicKey(),
|
||||
isRevoked: false,
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
}
|
||||
|
||||
try {
|
||||
this.selfContact = await this.singletons.findOrCreateSingleton<TrustedContactContent, TrustedContact>(
|
||||
TrustedContact.singletonPredicate,
|
||||
ContentType.TrustedContact,
|
||||
FillItemContent<TrustedContactContent>(content),
|
||||
)
|
||||
|
||||
this.shouldReloadSelfContact = false
|
||||
} finally {
|
||||
this.isReloadingSelfContact = false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const UnknownContactName = 'Unnamed contact'
|
||||
@@ -0,0 +1,61 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import {
|
||||
ContactPublicKeySet,
|
||||
FillItemContent,
|
||||
TrustedContactContent,
|
||||
TrustedContactContentSpecialized,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { FindTrustedContactUseCase } from './FindTrustedContact'
|
||||
import { UnknownContactName } from '../UnknownContactName'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { UpdateTrustedContactUseCase } from './UpdateTrustedContact'
|
||||
|
||||
export class CreateOrEditTrustedContactUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
name?: string
|
||||
contactUuid: string
|
||||
publicKey: string
|
||||
signingPublicKey: string
|
||||
isMe?: boolean
|
||||
}): Promise<TrustedContactInterface | undefined> {
|
||||
const findUsecase = new FindTrustedContactUseCase(this.items)
|
||||
const existingContact = findUsecase.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
|
||||
}
|
||||
|
||||
const content: TrustedContactContentSpecialized = {
|
||||
name: params.name ?? UnknownContactName,
|
||||
publicKeySet: ContactPublicKeySet.FromJson({
|
||||
encryption: params.publicKey,
|
||||
signing: params.signingPublicKey,
|
||||
isRevoked: false,
|
||||
timestamp: new Date(),
|
||||
}),
|
||||
contactUuid: params.contactUuid,
|
||||
isMe: params.isMe ?? false,
|
||||
}
|
||||
|
||||
const contact = await this.mutator.createItem<TrustedContactInterface>(
|
||||
ContentType.TrustedContact,
|
||||
FillItemContent<TrustedContactContent>(content),
|
||||
true,
|
||||
)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return contact
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type FindContactQuery = { userUuid: string } | { signingPublicKey: string } | { publicKey: string }
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Predicate, TrustedContactInterface } from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FindContactQuery } from './FindContactQuery'
|
||||
|
||||
export class FindTrustedContactUseCase {
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(query: FindContactQuery): TrustedContactInterface | undefined {
|
||||
if ('userUuid' in query && query.userUuid) {
|
||||
return this.items.itemsMatchingPredicate<TrustedContactInterface>(
|
||||
ContentType.TrustedContact,
|
||||
new Predicate<TrustedContactInterface>('contactUuid', '=', query.userUuid),
|
||||
)[0]
|
||||
}
|
||||
|
||||
if ('signingPublicKey' in query && query.signingPublicKey) {
|
||||
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TrustedContact)
|
||||
return allContacts.find((contact) => contact.isSigningKeyTrusted(query.signingPublicKey))
|
||||
}
|
||||
|
||||
if ('publicKey' in query && query.publicKey) {
|
||||
const allContacts = this.items.getItems<TrustedContactInterface>(ContentType.TrustedContact)
|
||||
return allContacts.find((contact) => contact.isPublicKeyTrusted(query.publicKey))
|
||||
}
|
||||
|
||||
throw new Error('Invalid query')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
|
||||
import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models'
|
||||
|
||||
export class UpdateTrustedContactUseCase {
|
||||
constructor(private mutator: MutatorClientInterface, private sync: SyncServiceInterface) {}
|
||||
|
||||
async execute(
|
||||
contact: TrustedContactInterface,
|
||||
params: { name: string; publicKey: string; signingPublicKey: string },
|
||||
): Promise<TrustedContactInterface> {
|
||||
const updatedContact = await this.mutator.changeItem<TrustedContactMutator, TrustedContactInterface>(
|
||||
contact,
|
||||
(mutator) => {
|
||||
mutator.name = params.name
|
||||
if (
|
||||
params.publicKey !== contact.publicKeySet.encryption ||
|
||||
params.signingPublicKey !== contact.publicKeySet.signing
|
||||
) {
|
||||
mutator.addPublicKey({
|
||||
encryption: params.publicKey,
|
||||
signing: params.signingPublicKey,
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return updatedContact
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
PayloadSource,
|
||||
PersistentSignatureData,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../../Item/ItemManagerInterface'
|
||||
import { ValidateItemSignerUseCase } from './ValidateItemSigner'
|
||||
|
||||
describe('validate item signer use case', () => {
|
||||
let usecase: ValidateItemSignerUseCase
|
||||
let items: ItemManagerInterface
|
||||
|
||||
const trustedContact = {} as jest.Mocked<TrustedContactInterface>
|
||||
trustedContact.isSigningKeyTrusted = jest.fn().mockReturnValue(true)
|
||||
|
||||
beforeEach(() => {
|
||||
items = {} as jest.Mocked<ItemManagerInterface>
|
||||
usecase = new ValidateItemSignerUseCase(items)
|
||||
})
|
||||
|
||||
const createItem = (params: {
|
||||
last_edited_by_uuid: string | undefined
|
||||
shared_vault_uuid: string | undefined
|
||||
signatureData: PersistentSignatureData | undefined
|
||||
source?: PayloadSource
|
||||
}): jest.Mocked<DecryptedItemInterface> => {
|
||||
const payload = {
|
||||
source: params.source ?? PayloadSource.RemoteRetrieved,
|
||||
} as jest.Mocked<DecryptedItemInterface['payload']>
|
||||
|
||||
const item = {
|
||||
last_edited_by_uuid: params.last_edited_by_uuid,
|
||||
shared_vault_uuid: params.shared_vault_uuid,
|
||||
signatureData: params.signatureData,
|
||||
payload: payload,
|
||||
} as unknown as jest.Mocked<DecryptedItemInterface>
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
describe('has last edited by uuid', () => {
|
||||
describe('trusted contact not found', () => {
|
||||
beforeEach(() => {
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([])
|
||||
})
|
||||
|
||||
it('should return invalid signing is required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return not applicable signing is not required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: undefined,
|
||||
signatureData: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
})
|
||||
|
||||
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({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
source: PayloadSource.Constructor,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
|
||||
it('should return not applicable if the item was just recently saved', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
source: PayloadSource.RemoteSaved,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
|
||||
it('should return invalid if signing is required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
signatureData: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('has signature data', () => {
|
||||
describe('signature data does not have result', () => {
|
||||
it('should return invalid if signing is required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: undefined,
|
||||
signatureData: {
|
||||
required: false,
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('signature data has result', () => {
|
||||
it('should return invalid if signature result does not pass', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: false,
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: true,
|
||||
publicKey: 'pk-123',
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: 'uuid-123',
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: true,
|
||||
publicKey: 'pk-123',
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact])
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('yes')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no last edited by uuid', () => {
|
||||
describe('does not have signature data', () => {
|
||||
it('should return not applicable if the item was just recently created', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
source: PayloadSource.Constructor,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
|
||||
it('should return not applicable if the item was just recently saved', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
source: PayloadSource.RemoteSaved,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
|
||||
it('should return invalid if signing is required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
signatureData: undefined,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('has signature data', () => {
|
||||
describe('signature data does not have result', () => {
|
||||
it('should return invalid if signing is required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return not applicable if signing is not required', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
signatureData: {
|
||||
required: false,
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('not-applicable')
|
||||
})
|
||||
})
|
||||
|
||||
describe('signature data has result', () => {
|
||||
it('should return invalid if signature result does not pass', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: false,
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: true,
|
||||
publicKey: 'pk-123',
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.getItems = jest.fn().mockReturnValue([])
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('no')
|
||||
})
|
||||
|
||||
it('should return valid if signature result passes and a trusted contact is found for signature public key', () => {
|
||||
const item = createItem({
|
||||
last_edited_by_uuid: undefined,
|
||||
shared_vault_uuid: 'shared-vault-123',
|
||||
signatureData: {
|
||||
required: true,
|
||||
result: {
|
||||
passes: true,
|
||||
publicKey: 'pk-123',
|
||||
},
|
||||
} as jest.Mocked<PersistentSignatureData>,
|
||||
})
|
||||
|
||||
items.getItems = jest.fn().mockReturnValue([trustedContact])
|
||||
|
||||
const result = usecase.execute(item)
|
||||
expect(result).toEqual('yes')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
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'
|
||||
|
||||
export class ValidateItemSignerUseCase {
|
||||
private findContactUseCase = new FindTrustedContactUseCase(this.items)
|
||||
|
||||
constructor(private items: ItemManagerInterface) {}
|
||||
|
||||
execute(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
const uuidOfLastEditor = item.last_edited_by_uuid
|
||||
if (uuidOfLastEditor) {
|
||||
return this.validateSignatureWithLastEditedByUuid(item, uuidOfLastEditor)
|
||||
} else {
|
||||
return this.validateSignatureWithNoLastEditedByUuid(item)
|
||||
}
|
||||
}
|
||||
|
||||
private isItemLocallyCreatedAndDoesNotRequireSignature(item: DecryptedItemInterface): boolean {
|
||||
return item.payload.source === PayloadSource.Constructor
|
||||
}
|
||||
|
||||
private isItemResutOfRemoteSaveAndDoesNotRequireSignature(item: DecryptedItemInterface): boolean {
|
||||
return item.payload.source === PayloadSource.RemoteSaved
|
||||
}
|
||||
|
||||
private validateSignatureWithLastEditedByUuid(
|
||||
item: DecryptedItemInterface,
|
||||
uuidOfLastEditor: string,
|
||||
): ValidateItemSignerResult {
|
||||
const requiresSignature = doesPayloadRequireSigning(item)
|
||||
|
||||
const trustedContact = this.findContactUseCase.execute({ userUuid: uuidOfLastEditor })
|
||||
if (!trustedContact) {
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
} else {
|
||||
return 'not-applicable'
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.signatureData) {
|
||||
if (
|
||||
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
|
||||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
|
||||
) {
|
||||
return 'not-applicable'
|
||||
}
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
}
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
const signatureData = item.signatureData
|
||||
if (!signatureData.result) {
|
||||
if (signatureData.required) {
|
||||
return 'no'
|
||||
}
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
const signatureResult = signatureData.result
|
||||
|
||||
if (!signatureResult.passes) {
|
||||
return 'no'
|
||||
}
|
||||
|
||||
const signerPublicKey = signatureResult.publicKey
|
||||
|
||||
if (trustedContact.isSigningKeyTrusted(signerPublicKey)) {
|
||||
return 'yes'
|
||||
}
|
||||
|
||||
return 'no'
|
||||
}
|
||||
|
||||
private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ValidateItemSignerResult {
|
||||
const requiresSignature = doesPayloadRequireSigning(item)
|
||||
|
||||
if (!item.signatureData) {
|
||||
if (
|
||||
this.isItemLocallyCreatedAndDoesNotRequireSignature(item) ||
|
||||
this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item)
|
||||
) {
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
if (requiresSignature) {
|
||||
return 'no'
|
||||
}
|
||||
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
const signatureData = item.signatureData
|
||||
if (!signatureData.result) {
|
||||
if (signatureData.required) {
|
||||
return 'no'
|
||||
}
|
||||
return 'not-applicable'
|
||||
}
|
||||
|
||||
const signatureResult = signatureData.result
|
||||
|
||||
if (!signatureResult.passes) {
|
||||
return 'no'
|
||||
}
|
||||
|
||||
const signerPublicKey = signatureResult.publicKey
|
||||
|
||||
const trustedContact = this.findContactUseCase.execute({ signingPublicKey: signerPublicKey })
|
||||
|
||||
if (trustedContact) {
|
||||
return 'yes'
|
||||
}
|
||||
|
||||
return 'no'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type ValidateItemSignerResult = 'not-applicable' | 'yes' | 'no'
|
||||
@@ -18,6 +18,8 @@ export function isChunkFullEntry(
|
||||
export type DatabaseKeysLoadChunkResponse = {
|
||||
keys: {
|
||||
itemsKeys: DatabaseKeysLoadChunk
|
||||
keySystemRootKeys: DatabaseKeysLoadChunk
|
||||
keySystemItemsKeys: DatabaseKeysLoadChunk
|
||||
remainingChunks: DatabaseKeysLoadChunk[]
|
||||
}
|
||||
remainingChunksItemCount: number
|
||||
@@ -26,6 +28,8 @@ export type DatabaseKeysLoadChunkResponse = {
|
||||
export type DatabaseFullEntryLoadChunkResponse = {
|
||||
fullEntries: {
|
||||
itemsKeys: DatabaseFullEntryLoadChunk
|
||||
keySystemRootKeys: DatabaseFullEntryLoadChunk
|
||||
keySystemItemsKeys: DatabaseFullEntryLoadChunk
|
||||
remainingChunks: DatabaseFullEntryLoadChunk[]
|
||||
}
|
||||
remainingChunksItemCount: number
|
||||
|
||||
@@ -83,17 +83,25 @@ export function GetSortedPayloadsByPriority<T extends DatabaseItemMetadata = Dat
|
||||
options: DatabaseLoadOptions,
|
||||
): {
|
||||
itemsKeyPayloads: T[]
|
||||
keySystemRootKeyPayloads: T[]
|
||||
keySystemItemsKeyPayloads: T[]
|
||||
contentTypePriorityPayloads: T[]
|
||||
remainingPayloads: T[]
|
||||
} {
|
||||
const itemsKeyPayloads: T[] = []
|
||||
const keySystemRootKeyPayloads: T[] = []
|
||||
const keySystemItemsKeyPayloads: T[] = []
|
||||
const contentTypePriorityPayloads: T[] = []
|
||||
const remainingPayloads: T[] = []
|
||||
|
||||
for (let index = 0; index < payloads.length; index++) {
|
||||
const payload = payloads[index]
|
||||
|
||||
if (payload.content_type === ContentType.ItemsKey) {
|
||||
if (payload.content_type === ContentType.KeySystemRootKey) {
|
||||
keySystemRootKeyPayloads.push(payload)
|
||||
} else if (payload.content_type === ContentType.KeySystemItemsKey) {
|
||||
keySystemItemsKeyPayloads.push(payload)
|
||||
} else if (payload.content_type === ContentType.ItemsKey) {
|
||||
itemsKeyPayloads.push(payload)
|
||||
} else if (options.contentTypePriority.includes(payload.content_type)) {
|
||||
contentTypePriorityPayloads.push(payload)
|
||||
@@ -104,6 +112,8 @@ export function GetSortedPayloadsByPriority<T extends DatabaseItemMetadata = Dat
|
||||
|
||||
return {
|
||||
itemsKeyPayloads,
|
||||
keySystemRootKeyPayloads,
|
||||
keySystemItemsKeyPayloads,
|
||||
contentTypePriorityPayloads: SortPayloadsByRecentAndContentPriority(
|
||||
contentTypePriorityPayloads,
|
||||
options.contentTypePriority,
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
compareVersions,
|
||||
ContentType,
|
||||
leftVersionGreaterThanOrEqualToRight,
|
||||
ProtocolVersion,
|
||||
} from '@standardnotes/common'
|
||||
import {
|
||||
BackupFileType,
|
||||
ContentTypeUsesRootKeyEncryption,
|
||||
CreateAnyKeyParams,
|
||||
isItemsKey,
|
||||
SNItemsKey,
|
||||
SNRootKey,
|
||||
SNRootKeyParams,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
BackupFile,
|
||||
CreateDecryptedItemFromPayload,
|
||||
CreatePayloadSplit,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isDecryptedTransferPayload,
|
||||
isEncryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { extendArray } from '@standardnotes/utils'
|
||||
import { EncryptionService } from './EncryptionService'
|
||||
|
||||
export async function DecryptBackupFile(
|
||||
file: BackupFile,
|
||||
protocolService: EncryptionService,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return new EncryptedPayload(item)
|
||||
} else if (isDecryptedTransferPayload(item)) {
|
||||
return new DecryptedPayload(item)
|
||||
} else {
|
||||
throw Error('Unhandled case in decryptBackupFile')
|
||||
}
|
||||
})
|
||||
|
||||
const { encrypted, decrypted } = CreatePayloadSplit(payloads)
|
||||
|
||||
const type = getBackupFileType(file, payloads)
|
||||
|
||||
switch (type) {
|
||||
case BackupFileType.Corrupt:
|
||||
return new ClientDisplayableError('Invalid backup file.')
|
||||
case BackupFileType.Encrypted: {
|
||||
if (!password) {
|
||||
throw Error('Attempting to decrypt encrypted file with no password')
|
||||
}
|
||||
|
||||
const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent
|
||||
|
||||
return [
|
||||
...decrypted,
|
||||
...(await decryptEncrypted(password, CreateAnyKeyParams(keyParamsData), encrypted, protocolService)),
|
||||
]
|
||||
}
|
||||
case BackupFileType.EncryptedWithNonEncryptedItemsKey:
|
||||
return [...decrypted, ...(await decryptEncryptedWithNonEncryptedItemsKey(payloads, protocolService))]
|
||||
case BackupFileType.FullyDecrypted:
|
||||
return [...decrypted, ...encrypted]
|
||||
}
|
||||
}
|
||||
|
||||
function getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType {
|
||||
if (file.keyParams || file.auth_params) {
|
||||
return BackupFileType.Encrypted
|
||||
} else {
|
||||
const hasEncryptedItem = payloads.find(isEncryptedPayload)
|
||||
const hasDecryptedItemsKey = payloads.find(
|
||||
(payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload),
|
||||
)
|
||||
|
||||
if (hasEncryptedItem && hasDecryptedItemsKey) {
|
||||
return BackupFileType.EncryptedWithNonEncryptedItemsKey
|
||||
} else if (!hasEncryptedItem) {
|
||||
return BackupFileType.FullyDecrypted
|
||||
} else {
|
||||
return BackupFileType.Corrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptEncryptedWithNonEncryptedItemsKey(
|
||||
allPayloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[],
|
||||
protocolService: EncryptionService,
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const decryptedItemsKeys: DecryptedPayloadInterface<ItemsKeyContent>[] = []
|
||||
const encryptedPayloads: EncryptedPayloadInterface[] = []
|
||||
|
||||
allPayloads.forEach((payload) => {
|
||||
if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) {
|
||||
decryptedItemsKeys.push(payload as DecryptedPayloadInterface<ItemsKeyContent>)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
encryptedPayloads.push(payload)
|
||||
}
|
||||
})
|
||||
|
||||
const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload<ItemsKeyContent, SNItemsKey>(p))
|
||||
|
||||
return decryptWithItemsKeys(encryptedPayloads, itemsKeys, protocolService)
|
||||
}
|
||||
|
||||
function findKeyToUseForPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
availableKeys: ItemsKeyInterface[],
|
||||
protocolService: EncryptionService,
|
||||
keyParams?: SNRootKeyParams,
|
||||
fallbackRootKey?: SNRootKey,
|
||||
): ItemsKeyInterface | SNRootKey | undefined {
|
||||
let itemsKey: ItemsKeyInterface | SNRootKey | undefined
|
||||
|
||||
if (payload.items_key_id) {
|
||||
itemsKey = protocolService.itemsKeyForPayload(payload)
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
}
|
||||
|
||||
itemsKey = availableKeys.find((itemsKeyPayload) => {
|
||||
return payload.items_key_id === itemsKeyPayload.uuid
|
||||
})
|
||||
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadVersion = payload.version as ProtocolVersion
|
||||
|
||||
/**
|
||||
* Payloads with versions <= 003 use root key directly for encryption.
|
||||
* However, if the incoming key params are >= 004, this means we should
|
||||
* have an items key based off the 003 root key. We can't use the 004
|
||||
* root key directly because it's missing dataAuthenticationKey.
|
||||
*/
|
||||
if (leftVersionGreaterThanOrEqualToRight(keyParams.version, ProtocolVersion.V004)) {
|
||||
itemsKey = protocolService.defaultItemsKeyForItemVersion(payloadVersion, availableKeys)
|
||||
} else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) {
|
||||
itemsKey = fallbackRootKey
|
||||
}
|
||||
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
async function decryptWithItemsKeys(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
itemsKeys: ItemsKeyInterface[],
|
||||
protocolService: EncryptionService,
|
||||
keyParams?: SNRootKeyParams,
|
||||
fallbackRootKey?: SNRootKey,
|
||||
): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> {
|
||||
const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = []
|
||||
|
||||
for (const encryptedPayload of payloads) {
|
||||
if (ContentTypeUsesRootKeyEncryption(encryptedPayload.content_type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const key = findKeyToUseForPayload(encryptedPayload, itemsKeys, protocolService, keyParams, fallbackRootKey)
|
||||
|
||||
if (!key) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isItemsKey(key)) {
|
||||
const decryptedPayload = await protocolService.decryptSplitSingle({
|
||||
usesItemsKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
} else {
|
||||
const decryptedPayload = await protocolService.decryptSplitSingle({
|
||||
usesRootKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
}
|
||||
} catch (e) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
console.error('Error decrypting payload', encryptedPayload, e)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
async function decryptEncrypted(
|
||||
password: string,
|
||||
keyParams: SNRootKeyParams,
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
protocolService: EncryptionService,
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = []
|
||||
const rootKey = await protocolService.computeRootKey(password, keyParams)
|
||||
|
||||
const itemsKeysPayloads = payloads.filter((payload) => {
|
||||
return payload.content_type === ContentType.ItemsKey
|
||||
})
|
||||
|
||||
const itemsKeysDecryptionResults = await protocolService.decryptSplit({
|
||||
usesRootKey: {
|
||||
items: itemsKeysPayloads,
|
||||
key: rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
extendArray(results, itemsKeysDecryptionResults)
|
||||
|
||||
const decryptedPayloads = await decryptWithItemsKeys(
|
||||
payloads,
|
||||
itemsKeysDecryptionResults.filter(isDecryptedPayload).map((p) => CreateDecryptedItemFromPayload(p)),
|
||||
protocolService,
|
||||
keyParams,
|
||||
rootKey,
|
||||
)
|
||||
|
||||
extendArray(results, decryptedPayloads)
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
compareVersions,
|
||||
ContentType,
|
||||
leftVersionGreaterThanOrEqualToRight,
|
||||
ProtocolVersion,
|
||||
} from '@standardnotes/common'
|
||||
import {
|
||||
BackupFileType,
|
||||
CreateAnyKeyParams,
|
||||
isItemsKey,
|
||||
isKeySystemItemsKey,
|
||||
SNItemsKey,
|
||||
SplitPayloadsByEncryptionType,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
ContentTypeUsesRootKeyEncryption,
|
||||
BackupFile,
|
||||
CreateDecryptedItemFromPayload,
|
||||
CreatePayloadSplit,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isDecryptedTransferPayload,
|
||||
isEncryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadInterface,
|
||||
KeySystemItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
KeySystemRootKeyInterface,
|
||||
isKeySystemRootKey,
|
||||
RootKeyParamsInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { extendArray } from '@standardnotes/utils'
|
||||
import { EncryptionService } from './EncryptionService'
|
||||
|
||||
export class DecryptBackupFileUseCase {
|
||||
constructor(private encryption: EncryptionService) {}
|
||||
|
||||
async execute(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return new EncryptedPayload(item)
|
||||
} else if (isDecryptedTransferPayload(item)) {
|
||||
return new DecryptedPayload(item)
|
||||
} else {
|
||||
throw Error('Unhandled case in decryptBackupFile')
|
||||
}
|
||||
})
|
||||
|
||||
const { encrypted, decrypted } = CreatePayloadSplit(payloads)
|
||||
|
||||
const type = this.getBackupFileType(file, payloads)
|
||||
|
||||
switch (type) {
|
||||
case BackupFileType.Corrupt:
|
||||
return new ClientDisplayableError('Invalid backup file.')
|
||||
case BackupFileType.Encrypted: {
|
||||
if (!password) {
|
||||
throw Error('Attempting to decrypt encrypted file with no password')
|
||||
}
|
||||
|
||||
const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent
|
||||
|
||||
const rootKey = await this.encryption.computeRootKey(password, CreateAnyKeyParams(keyParamsData))
|
||||
|
||||
const results = await this.decryptEncrypted({
|
||||
password,
|
||||
payloads: encrypted,
|
||||
rootKey,
|
||||
keyParams: CreateAnyKeyParams(keyParamsData),
|
||||
})
|
||||
|
||||
return [...decrypted, ...results]
|
||||
}
|
||||
case BackupFileType.EncryptedWithNonEncryptedItemsKey:
|
||||
return [...decrypted, ...(await this.decryptEncryptedWithNonEncryptedItemsKey(payloads))]
|
||||
case BackupFileType.FullyDecrypted:
|
||||
return [...decrypted, ...encrypted]
|
||||
}
|
||||
}
|
||||
|
||||
private getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType {
|
||||
if (file.keyParams || file.auth_params) {
|
||||
return BackupFileType.Encrypted
|
||||
} else {
|
||||
const hasEncryptedItem = payloads.find(isEncryptedPayload)
|
||||
const hasDecryptedItemsKey = payloads.find(
|
||||
(payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload),
|
||||
)
|
||||
|
||||
if (hasEncryptedItem && hasDecryptedItemsKey) {
|
||||
return BackupFileType.EncryptedWithNonEncryptedItemsKey
|
||||
} else if (!hasEncryptedItem) {
|
||||
return BackupFileType.FullyDecrypted
|
||||
} else {
|
||||
return BackupFileType.Corrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptEncrypted(dto: {
|
||||
password: string
|
||||
keyParams: RootKeyParamsInterface
|
||||
payloads: EncryptedPayloadInterface[]
|
||||
rootKey: RootKeyInterface
|
||||
}): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = []
|
||||
|
||||
const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(dto.payloads)
|
||||
|
||||
const rootKeyBasedDecryptionResults = await this.encryption.decryptSplit({
|
||||
usesRootKey: {
|
||||
items: rootKeyEncryption || [],
|
||||
key: dto.rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
extendArray(results, rootKeyBasedDecryptionResults)
|
||||
|
||||
const decryptedPayloads = await this.decrypt({
|
||||
payloads: itemsKeyEncryption || [],
|
||||
availableItemsKeys: rootKeyBasedDecryptionResults
|
||||
.filter(isItemsKey)
|
||||
.filter(isDecryptedPayload)
|
||||
.map((p) => CreateDecryptedItemFromPayload(p)),
|
||||
keyParams: dto.keyParams,
|
||||
rootKey: dto.rootKey,
|
||||
})
|
||||
|
||||
extendArray(results, decryptedPayloads)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private async decryptEncryptedWithNonEncryptedItemsKey(
|
||||
payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[],
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const decryptedItemsKeys: DecryptedPayloadInterface<ItemsKeyContent>[] = []
|
||||
const encryptedPayloads: EncryptedPayloadInterface[] = []
|
||||
|
||||
payloads.forEach((payload) => {
|
||||
if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) {
|
||||
decryptedItemsKeys.push(payload as DecryptedPayloadInterface<ItemsKeyContent>)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
encryptedPayloads.push(payload)
|
||||
}
|
||||
})
|
||||
|
||||
const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload<ItemsKeyContent, SNItemsKey>(p))
|
||||
|
||||
return this.decrypt({ payloads: encryptedPayloads, availableItemsKeys: itemsKeys, rootKey: undefined })
|
||||
}
|
||||
|
||||
private findKeyToUseForPayload(dto: {
|
||||
payload: EncryptedPayloadInterface
|
||||
availableKeys: ItemsKeyInterface[]
|
||||
keyParams?: RootKeyParamsInterface
|
||||
rootKey?: RootKeyInterface
|
||||
}): ItemsKeyInterface | RootKeyInterface | KeySystemRootKeyInterface | KeySystemItemsKeyInterface | undefined {
|
||||
if (ContentTypeUsesRootKeyEncryption(dto.payload.content_type)) {
|
||||
if (!dto.rootKey) {
|
||||
throw new Error('Attempting to decrypt root key encrypted payload with no root key')
|
||||
}
|
||||
return dto.rootKey
|
||||
}
|
||||
|
||||
if (ContentTypeUsesKeySystemRootKeyEncryption(dto.payload.content_type)) {
|
||||
throw new Error('Backup file key system root key encryption is not supported')
|
||||
}
|
||||
|
||||
let itemsKey: ItemsKeyInterface | RootKeyInterface | KeySystemItemsKeyInterface | undefined
|
||||
|
||||
if (dto.payload.items_key_id) {
|
||||
itemsKey = this.encryption.itemsKeyForEncryptedPayload(dto.payload)
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
}
|
||||
|
||||
itemsKey = dto.availableKeys.find((itemsKeyPayload) => {
|
||||
return dto.payload.items_key_id === itemsKeyPayload.uuid
|
||||
})
|
||||
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
if (!dto.keyParams) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadVersion = dto.payload.version as ProtocolVersion
|
||||
|
||||
/**
|
||||
* Payloads with versions <= 003 use root key directly for encryption.
|
||||
* However, if the incoming key params are >= 004, this means we should
|
||||
* have an items key based off the 003 root key. We can't use the 004
|
||||
* root key directly because it's missing dataAuthenticationKey.
|
||||
*/
|
||||
if (leftVersionGreaterThanOrEqualToRight(dto.keyParams.version, ProtocolVersion.V004)) {
|
||||
itemsKey = this.encryption.defaultItemsKeyForItemVersion(payloadVersion, dto.availableKeys)
|
||||
} else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) {
|
||||
itemsKey = dto.rootKey
|
||||
}
|
||||
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
private async decrypt(dto: {
|
||||
payloads: EncryptedPayloadInterface[]
|
||||
availableItemsKeys: ItemsKeyInterface[]
|
||||
rootKey: RootKeyInterface | undefined
|
||||
keyParams?: RootKeyParamsInterface
|
||||
}): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> {
|
||||
const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = []
|
||||
|
||||
for (const encryptedPayload of dto.payloads) {
|
||||
try {
|
||||
const key = this.findKeyToUseForPayload({
|
||||
payload: encryptedPayload,
|
||||
availableKeys: dto.availableItemsKeys,
|
||||
keyParams: dto.keyParams,
|
||||
rootKey: dto.rootKey,
|
||||
})
|
||||
|
||||
if (!key) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isItemsKey(key) || isKeySystemItemsKey(key)) {
|
||||
const decryptedPayload = await this.encryption.decryptSplitSingle({
|
||||
usesItemsKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
} else if (isKeySystemRootKey(key)) {
|
||||
const decryptedPayload = await this.encryption.decryptSplitSingle({
|
||||
usesKeySystemRootKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
} else {
|
||||
const decryptedPayload = await this.encryption.decryptSplitSingle({
|
||||
usesRootKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
}
|
||||
} catch (e) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
console.error('Error decrypting payload', encryptedPayload, e)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import {
|
||||
CreateAnyKeyParams,
|
||||
CreateEncryptionSplitWithKeyLookup,
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
encryptedParametersFromPayload,
|
||||
encryptedInputParametersFromPayload,
|
||||
EncryptionProviderInterface,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
@@ -23,6 +22,11 @@ import {
|
||||
SplitPayloadsByEncryptionType,
|
||||
V001Algorithm,
|
||||
V002Algorithm,
|
||||
PublicKeySet,
|
||||
EncryptedOutputParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
AsymmetricSignatureVerificationDetachedResult,
|
||||
AsymmetricallyEncryptedString,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
BackupFile,
|
||||
@@ -37,9 +41,15 @@ import {
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
KeySystemItemsKeyInterface,
|
||||
KeySystemIdentifier,
|
||||
AsymmetricMessagePayload,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyParamsInterface,
|
||||
TrustedContactInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
extendArray,
|
||||
isNotUndefined,
|
||||
@@ -68,10 +78,10 @@ import { DeviceInterface } from '../Device/DeviceInterface'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
|
||||
import { RootKeyEncryptionService } from './RootKeyEncryption'
|
||||
import { DecryptBackupFile } from './BackupFileDecryptor'
|
||||
import { DecryptBackupFileUseCase } from './DecryptBackupFileUseCase'
|
||||
import { EncryptionServiceEvent } from './EncryptionServiceEvent'
|
||||
import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters'
|
||||
|
||||
/**
|
||||
* The encryption service is responsible for the encryption and decryption of payloads, and
|
||||
@@ -108,9 +118,11 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
public deviceInterface: DeviceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
public readonly keys: KeySystemKeyManagerInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
public crypto: PureCryptoInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
@@ -125,17 +137,22 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
payloadManager,
|
||||
storageService,
|
||||
this.operatorManager,
|
||||
keys,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
this.rootKeyEncryption = new RootKeyEncryptionService(
|
||||
this.itemManager,
|
||||
this.mutator,
|
||||
this.operatorManager,
|
||||
this.deviceInterface,
|
||||
this.storageService,
|
||||
this.payloadManager,
|
||||
keys,
|
||||
this.identifier,
|
||||
this.internalEventBus,
|
||||
)
|
||||
|
||||
this.rootKeyObserverDisposer = this.rootKeyEncryption.addEventObserver((event) => {
|
||||
this.itemsEncryption.userVersion = this.getUserVersion()
|
||||
if (event === RootKeyServiceEvent.RootKeyStatusChanged) {
|
||||
@@ -166,6 +183,32 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
/** @throws */
|
||||
getKeyPair(): PkcKeyPair {
|
||||
const rootKey = this.getRootKey()
|
||||
|
||||
if (!rootKey?.encryptionKeyPair) {
|
||||
throw new Error('Account keypair not found')
|
||||
}
|
||||
|
||||
return rootKey.encryptionKeyPair
|
||||
}
|
||||
|
||||
/** @throws */
|
||||
getSigningKeyPair(): PkcKeyPair {
|
||||
const rootKey = this.getRootKey()
|
||||
|
||||
if (!rootKey?.signingKeyPair) {
|
||||
throw new Error('Account keypair not found')
|
||||
}
|
||||
|
||||
return rootKey.signingKeyPair
|
||||
}
|
||||
|
||||
hasSigningKeyPair(): boolean {
|
||||
return !!this.getRootKey()?.signingKeyPair
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
await this.rootKeyEncryption.initialize()
|
||||
}
|
||||
@@ -213,8 +256,12 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
|
||||
public async reencryptItemsKeys(): Promise<void> {
|
||||
await this.rootKeyEncryption.reencryptItemsKeys()
|
||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
||||
await this.rootKeyEncryption.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
}
|
||||
|
||||
public reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise<void> {
|
||||
return this.rootKeyEncryption.reencryptKeySystemItemsKeysForVault(keySystemIdentifier)
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
@@ -222,11 +269,14 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
await this.itemsEncryption.decryptErroredPayloads()
|
||||
await this.rootKeyEncryption.decryptErroredRootPayloads()
|
||||
await this.itemsEncryption.decryptErroredItemPayloads()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.itemsKeyForPayload(payload)
|
||||
public itemsKeyForEncryptedPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.itemsKeyForEncryptedPayload(payload)
|
||||
}
|
||||
|
||||
public defaultItemsKeyForItemVersion(
|
||||
@@ -241,34 +291,66 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
}
|
||||
|
||||
public async encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]> {
|
||||
const allEncryptedParams: EncryptedParameters[] = []
|
||||
const allEncryptedParams: EncryptedOutputParameters[] = []
|
||||
|
||||
if (split.usesRootKey) {
|
||||
const {
|
||||
usesRootKey,
|
||||
usesItemsKey,
|
||||
usesKeySystemRootKey,
|
||||
usesRootKeyWithKeyLookup,
|
||||
usesItemsKeyWithKeyLookup,
|
||||
usesKeySystemRootKeyWithKeyLookup,
|
||||
} = split
|
||||
|
||||
const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined
|
||||
|
||||
if (usesRootKey) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
|
||||
split.usesRootKey.items,
|
||||
split.usesRootKey.key,
|
||||
usesRootKey.items,
|
||||
usesRootKey.key,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
if (usesRootKeyWithKeyLookup) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
|
||||
usesRootKeyWithKeyLookup.items,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (usesKeySystemRootKey) {
|
||||
const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
|
||||
usesKeySystemRootKey.items,
|
||||
usesKeySystemRootKey.key,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, keySystemRootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (usesKeySystemRootKeyWithKeyLookup) {
|
||||
const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
|
||||
usesKeySystemRootKeyWithKeyLookup.items,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, keySystemRootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (usesItemsKey) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
usesItemsKey.items,
|
||||
usesItemsKey.key,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
if (usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
usesItemsKeyWithKeyLookup.items,
|
||||
signingKeyPair,
|
||||
)
|
||||
extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
@@ -300,32 +382,48 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
>(split: KeyedDecryptionSplit): Promise<(P | EncryptedPayloadInterface)[]> {
|
||||
const resultParams: (DecryptedParameters<C> | ErrorDecryptingParameters)[] = []
|
||||
|
||||
if (split.usesRootKey) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
|
||||
split.usesRootKey.items,
|
||||
split.usesRootKey.key,
|
||||
)
|
||||
const {
|
||||
usesRootKey,
|
||||
usesItemsKey,
|
||||
usesKeySystemRootKey,
|
||||
usesRootKeyWithKeyLookup,
|
||||
usesItemsKeyWithKeyLookup,
|
||||
usesKeySystemRootKeyWithKeyLookup,
|
||||
} = split
|
||||
|
||||
if (usesRootKey) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(usesRootKey.items, usesRootKey.key)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
if (usesRootKeyWithKeyLookup) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
if (usesKeySystemRootKey) {
|
||||
const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
|
||||
usesKeySystemRootKey.items,
|
||||
usesKeySystemRootKey.key,
|
||||
)
|
||||
extendArray(resultParams, keySystemRootKeyDecrypted)
|
||||
}
|
||||
if (usesKeySystemRootKeyWithKeyLookup) {
|
||||
const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
usesKeySystemRootKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, keySystemRootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (usesItemsKey) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(usesItemsKey.items, usesItemsKey.key)
|
||||
extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
if (usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
usesItemsKeyWithKeyLookup.items,
|
||||
)
|
||||
extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
@@ -349,6 +447,36 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
return packagedResults
|
||||
}
|
||||
|
||||
async decryptPayloadWithKeyLookup<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): Promise<{
|
||||
parameters: DecryptedParameters<C> | ErrorDecryptingParameters
|
||||
payload: P | EncryptedPayloadInterface
|
||||
}> {
|
||||
const decryptedParameters = await this.itemsEncryption.decryptPayloadWithKeyLookup<C>(payload)
|
||||
|
||||
if (isErrorDecryptingParameters(decryptedParameters)) {
|
||||
return {
|
||||
parameters: decryptedParameters,
|
||||
payload: new EncryptedPayload({
|
||||
...payload.ejected(),
|
||||
...decryptedParameters,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
parameters: decryptedParameters,
|
||||
payload: new DecryptedPayload<C>({
|
||||
...payload.ejected(),
|
||||
...decryptedParameters,
|
||||
}) as P,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user's account protocol version is not equal to the latest version.
|
||||
*/
|
||||
@@ -420,27 +548,130 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
* Computes a root key given a password and key params.
|
||||
* Delegates computation to respective protocol operator.
|
||||
*/
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
|
||||
public async computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
|
||||
return this.rootKeyEncryption.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root key using the latest protocol version
|
||||
*/
|
||||
public async createRootKey(
|
||||
public async createRootKey<K extends RootKeyInterface>(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
version?: ProtocolVersion,
|
||||
) {
|
||||
): Promise<K> {
|
||||
return this.rootKeyEncryption.createRootKey(identifier, password, origination, version)
|
||||
}
|
||||
|
||||
createRandomizedKeySystemRootKey(dto: {
|
||||
systemIdentifier: KeySystemIdentifier
|
||||
systemName: string
|
||||
systemDescription?: string
|
||||
}): KeySystemRootKeyInterface {
|
||||
return this.operatorManager.defaultOperator().createRandomizedKeySystemRootKey(dto)
|
||||
}
|
||||
|
||||
createUserInputtedKeySystemRootKey(dto: {
|
||||
systemIdentifier: KeySystemIdentifier
|
||||
systemName: string
|
||||
systemDescription?: string
|
||||
userInputtedPassword: string
|
||||
}): KeySystemRootKeyInterface {
|
||||
return this.operatorManager.defaultOperator().createUserInputtedKeySystemRootKey(dto)
|
||||
}
|
||||
|
||||
deriveUserInputtedKeySystemRootKey(dto: {
|
||||
keyParams: KeySystemRootKeyParamsInterface
|
||||
userInputtedPassword: string
|
||||
}): KeySystemRootKeyInterface {
|
||||
return this.operatorManager.defaultOperator().deriveUserInputtedKeySystemRootKey(dto)
|
||||
}
|
||||
|
||||
createKeySystemItemsKey(
|
||||
uuid: string,
|
||||
keySystemIdentifier: KeySystemIdentifier,
|
||||
sharedVaultUuid: string | undefined,
|
||||
rootKeyToken: string,
|
||||
): KeySystemItemsKeyInterface {
|
||||
return this.operatorManager
|
||||
.defaultOperator()
|
||||
.createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken)
|
||||
}
|
||||
|
||||
asymmetricallyEncryptMessage(dto: {
|
||||
message: AsymmetricMessagePayload
|
||||
senderKeyPair: PkcKeyPair
|
||||
senderSigningKeyPair: PkcKeyPair
|
||||
recipientPublicKey: string
|
||||
}): AsymmetricallyEncryptedString {
|
||||
const operator = this.operatorManager.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.operatorManager.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.encryptedString)
|
||||
const keyOperator = this.operatorManager.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 {
|
||||
const defaultOperator = this.operatorManager.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(encryptedString)
|
||||
const keyOperator = this.operatorManager.operatorForVersion(version)
|
||||
return keyOperator.asymmetricSignatureVerifyDetached(encryptedString)
|
||||
}
|
||||
|
||||
getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet {
|
||||
const defaultOperator = this.operatorManager.defaultOperator()
|
||||
const version = defaultOperator.versionForAsymmetricallyEncryptedString(string)
|
||||
|
||||
const keyOperator = this.operatorManager.operatorForVersion(version)
|
||||
return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string)
|
||||
}
|
||||
|
||||
public async decryptBackupFile(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface<ItemContent>)[]> {
|
||||
const result = await DecryptBackupFile(file, this, password)
|
||||
const usecase = new DecryptBackupFileUseCase(this)
|
||||
const result = await usecase.execute(file, password)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -468,7 +699,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
items: ejected,
|
||||
}
|
||||
|
||||
const keyParams = await this.getRootKeyParams()
|
||||
const keyParams = this.getRootKeyParams()
|
||||
data.keyParams = keyParams?.getPortableValue()
|
||||
return data
|
||||
}
|
||||
@@ -504,7 +735,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
return (await this.rootKeyEncryption.hasRootKeyWrapper()) && this.rootKeyEncryption.getRootKey() == undefined
|
||||
}
|
||||
|
||||
public async getRootKeyParams() {
|
||||
public getRootKeyParams() {
|
||||
return this.rootKeyEncryption.getRootKeyParams()
|
||||
}
|
||||
|
||||
@@ -517,7 +748,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
* Wrapping key params are read from disk.
|
||||
*/
|
||||
public async computeWrappingKey(passcode: string) {
|
||||
const keyParams = await this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
|
||||
const keyParams = this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
|
||||
const key = await this.computeRootKey(passcode, keyParams)
|
||||
return key
|
||||
}
|
||||
@@ -545,17 +776,21 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
await this.rootKeyEncryption.removeRootKeyWrapper()
|
||||
}
|
||||
|
||||
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
public async setRootKey(key: RootKeyInterface, wrappingKey?: SNRootKey) {
|
||||
await this.rootKeyEncryption.setRootKey(key, wrappingKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-memory root key value.
|
||||
*/
|
||||
public getRootKey() {
|
||||
public getRootKey(): RootKeyInterface | undefined {
|
||||
return this.rootKeyEncryption.getRootKey()
|
||||
}
|
||||
|
||||
public getSureRootKey(): RootKeyInterface {
|
||||
return this.rootKeyEncryption.getRootKey() as RootKeyInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes root key and wrapper from keychain. Used when signing out of application.
|
||||
*/
|
||||
@@ -571,26 +806,31 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
return this.rootKeyEncryption.validatePasscode(passcode)
|
||||
}
|
||||
|
||||
public getEmbeddedPayloadAuthenticatedData(
|
||||
public getEmbeddedPayloadAuthenticatedData<D extends ItemAuthenticatedData>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
): D | undefined {
|
||||
const version = payload.version
|
||||
if (!version) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
const authenticatedData = operator.getPayloadAuthenticatedData(encryptedParametersFromPayload(payload))
|
||||
return authenticatedData
|
||||
|
||||
const authenticatedData = operator.getPayloadAuthenticatedDataForExternalUse(
|
||||
encryptedInputParametersFromPayload(payload),
|
||||
)
|
||||
|
||||
return authenticatedData as D
|
||||
}
|
||||
|
||||
/** Returns the key params attached to this key's encrypted payload */
|
||||
public getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined {
|
||||
public getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined {
|
||||
const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key)
|
||||
if (!authenticatedData) {
|
||||
return undefined
|
||||
}
|
||||
if (isVersionLessThanOrEqualTo(key.version, ProtocolVersion.V003)) {
|
||||
const rawKeyParams = authenticatedData as LegacyAttachedData
|
||||
const rawKeyParams = authenticatedData as unknown as LegacyAttachedData
|
||||
return this.createKeyParams(rawKeyParams)
|
||||
} else {
|
||||
const rawKeyParams = (authenticatedData as RootKeyEncryptedAuthenticatedData).kp
|
||||
@@ -683,7 +923,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
const hasSyncedItemsKey = !isNullOrUndefined(defaultSyncedKey)
|
||||
if (hasSyncedItemsKey) {
|
||||
/** Delete all never synced keys */
|
||||
await this.itemManager.setItemsToBeDeleted(neverSyncedKeys)
|
||||
await this.mutator.setItemsToBeDeleted(neverSyncedKeys)
|
||||
} else {
|
||||
/**
|
||||
* No previous synced items key.
|
||||
@@ -692,14 +932,14 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
* we end up with 0 items keys, create a new one. This covers the case when you open
|
||||
* the app offline and it creates an 004 key, and then you sign into an 003 account.
|
||||
*/
|
||||
const rootKeyParams = await this.getRootKeyParams()
|
||||
const rootKeyParams = this.getRootKeyParams()
|
||||
if (rootKeyParams) {
|
||||
/** If neverSynced.version != rootKey.version, delete. */
|
||||
const toDelete = neverSyncedKeys.filter((itemsKey) => {
|
||||
return itemsKey.keyVersion !== rootKeyParams.version
|
||||
})
|
||||
if (toDelete.length > 0) {
|
||||
await this.itemManager.setItemsToBeDeleted(toDelete)
|
||||
await this.mutator.setItemsToBeDeleted(toDelete)
|
||||
}
|
||||
|
||||
if (this.itemsEncryption.getItemsKeys().length === 0) {
|
||||
@@ -741,26 +981,7 @@ export class EncryptionService extends AbstractService<EncryptionServiceEvent> i
|
||||
|
||||
const unsyncedKeys = this.itemsEncryption.getItemsKeys().filter((key) => key.neverSynced && !key.dirty)
|
||||
if (unsyncedKeys.length > 0) {
|
||||
void this.itemManager.setItemsDirty(unsyncedKeys)
|
||||
}
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return {
|
||||
encryption: {
|
||||
getLatestVersion: this.getLatestVersion(),
|
||||
hasAccount: this.hasAccount(),
|
||||
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
|
||||
getUserVersion: this.getUserVersion(),
|
||||
upgradeAvailable: await this.upgradeAvailable(),
|
||||
accountUpgradeAvailable: this.accountUpgradeAvailable(),
|
||||
passcodeUpgradeAvailable: await this.passcodeUpgradeAvailable(),
|
||||
hasPasscode: this.hasPasscode(),
|
||||
isPasscodeLocked: await this.isPasscodeLocked(),
|
||||
needsNewRootKeyBasedItemsKey: this.needsNewRootKeyBasedItemsKey(),
|
||||
...(await this.itemsEncryption.getDiagnostics()),
|
||||
...(await this.rootKeyEncryption.getDiagnostics()),
|
||||
},
|
||||
void this.mutator.setItemsDirty(unsyncedKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function DecryptItemsKeyByPromptingUser(
|
||||
| 'aborted'
|
||||
> {
|
||||
if (!keyParams) {
|
||||
keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey)
|
||||
keyParams = encryptor.getKeyEmbeddedKeyParamsFromItemsKey(itemsKey)
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
isErrorDecryptingParameters,
|
||||
@@ -9,26 +8,30 @@ import {
|
||||
StandardException,
|
||||
encryptPayload,
|
||||
decryptPayload,
|
||||
EncryptedOutputParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayload,
|
||||
EncryptedPayloadInterface,
|
||||
KeySystemRootKeyInterface,
|
||||
isEncryptedPayload,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadEmitSource,
|
||||
KeySystemItemsKeyInterface,
|
||||
SureFindPayload,
|
||||
ContentTypeUsesRootKeyEncryption,
|
||||
} from '@standardnotes/models'
|
||||
import { Uuids } from '@standardnotes/utils'
|
||||
|
||||
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export class ItemsEncryptionService extends AbstractService {
|
||||
private removeItemsObserver!: () => void
|
||||
@@ -39,13 +42,14 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
private keys: KeySystemKeyManagerInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeItemsObserver = this.itemManager.addObserver([ContentType.ItemsKey], ({ changed, inserted }) => {
|
||||
if (changed.concat(inserted).length > 0) {
|
||||
void this.decryptErroredPayloads()
|
||||
void this.decryptErroredItemPayloads()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -54,6 +58,8 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.operatorManager as unknown) = undefined
|
||||
;(this.keys as unknown) = undefined
|
||||
this.removeItemsObserver()
|
||||
;(this.removeItemsObserver as unknown) = undefined
|
||||
super.deinit()
|
||||
@@ -70,12 +76,17 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
return this.storageService.savePayloads(payloads)
|
||||
}
|
||||
|
||||
public getItemsKeys() {
|
||||
public getItemsKeys(): ItemsKeyInterface[] {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
return this.getItemsKeys().find(
|
||||
public itemsKeyForEncryptedPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
const keySystemItemsKeys = this.itemManager.getItems<KeySystemItemsKeyInterface>(ContentType.KeySystemItemsKey)
|
||||
|
||||
return [...itemsKeys, ...keySystemItemsKeys].find(
|
||||
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
|
||||
)
|
||||
}
|
||||
@@ -84,8 +95,20 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
return findDefaultItemsKey(this.getItemsKeys())
|
||||
}
|
||||
|
||||
private keyToUseForItemEncryption(): ItemsKeyInterface | StandardException {
|
||||
private keyToUseForItemEncryption(
|
||||
payload: DecryptedPayloadInterface,
|
||||
): ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | StandardException {
|
||||
if (payload.key_system_identifier) {
|
||||
const keySystemItemsKey = this.keys.getPrimaryKeySystemItemsKey(payload.key_system_identifier)
|
||||
if (!keySystemItemsKey) {
|
||||
return new StandardException('Cannot find key system items key to use for encryption')
|
||||
}
|
||||
|
||||
return keySystemItemsKey
|
||||
}
|
||||
|
||||
const defaultKey = this.getDefaultItemsKey()
|
||||
|
||||
let result: ItemsKeyInterface | undefined = undefined
|
||||
|
||||
if (this.userVersion && this.userVersion !== defaultKey?.keyVersion) {
|
||||
@@ -107,9 +130,11 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
return result
|
||||
}
|
||||
|
||||
private keyToUseForDecryptionOfPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined {
|
||||
private keyToUseForDecryptionOfPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined {
|
||||
if (payload.items_key_id) {
|
||||
const itemsKey = this.itemsKeyForPayload(payload)
|
||||
const itemsKey = this.itemsKeyForEncryptedPayload(payload)
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
@@ -117,20 +142,24 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
return defaultKey
|
||||
}
|
||||
|
||||
public async encryptPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.keyToUseForItemEncryption()
|
||||
public async encryptPayloadWithKeyLookup(
|
||||
payload: DecryptedPayloadInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters> {
|
||||
const key = this.keyToUseForItemEncryption(payload)
|
||||
|
||||
if (key instanceof StandardException) {
|
||||
throw Error(key.message)
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
return this.encryptPayload(payload, key, signingKeyPair)
|
||||
}
|
||||
|
||||
public async encryptPayload(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters> {
|
||||
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters> {
|
||||
if (isEncryptedPayload(payload)) {
|
||||
throw Error('Attempting to encrypt already encrypted payload.')
|
||||
}
|
||||
@@ -141,18 +170,22 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
throw Error('Attempting to encrypt payload with no UuidGenerator.')
|
||||
}
|
||||
|
||||
return encryptPayload(payload, key, this.operatorManager)
|
||||
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
|
||||
}
|
||||
|
||||
public async encryptPayloads(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
key: ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair)))
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload)))
|
||||
public async encryptPayloadsWithKeyLookup(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
@@ -173,7 +206,7 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
|
||||
public async decryptPayload<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
key: ItemsKeyInterface,
|
||||
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!payload.content) {
|
||||
return {
|
||||
@@ -193,21 +226,24 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
|
||||
public async decryptPayloads<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
key: ItemsKeyInterface,
|
||||
key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface,
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
const payloads = this.payloadManager.invalidPayloads.filter((i) => i.content_type !== ContentType.ItemsKey)
|
||||
if (payloads.length === 0) {
|
||||
public async decryptErroredItemPayloads(): Promise<void> {
|
||||
const erroredItemPayloads = this.payloadManager.invalidPayloads.filter(
|
||||
(i) =>
|
||||
!ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
|
||||
)
|
||||
if (erroredItemPayloads.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const resultParams = await this.decryptPayloadsWithKeyLookup(payloads)
|
||||
const resultParams = await this.decryptPayloadsWithKeyLookup(erroredItemPayloads)
|
||||
|
||||
const decryptedPayloads = resultParams.map((params) => {
|
||||
const original = SureFindPayload(payloads, params.uuid)
|
||||
const original = SureFindPayload(erroredItemPayloads, params.uuid)
|
||||
if (isErrorDecryptingParameters(params)) {
|
||||
return new EncryptedPayload({
|
||||
...original.ejected(),
|
||||
@@ -247,15 +283,4 @@ export class ItemsEncryptionService extends AbstractService {
|
||||
return key.keyVersion === version
|
||||
})
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
const keyForItems = this.keyToUseForItemEncryption()
|
||||
return {
|
||||
itemsEncryption: {
|
||||
itemsKeysIds: Uuids(this.getItemsKeys()),
|
||||
defaultItemsKeyId: this.getDefaultItemsKey()?.uuid,
|
||||
keyToUseForItemEncryptionId: keyForItems instanceof StandardException ? undefined : keyForItems.uuid,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import {
|
||||
ApplicationIdentifier,
|
||||
ProtocolVersionLatest,
|
||||
@@ -17,15 +18,19 @@ import {
|
||||
CreateAnyKeyParams,
|
||||
SNRootKey,
|
||||
isErrorDecryptingParameters,
|
||||
EncryptedParameters,
|
||||
DecryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
findDefaultItemsKey,
|
||||
ItemsKeyMutator,
|
||||
encryptPayload,
|
||||
decryptPayload,
|
||||
EncryptedOutputParameters,
|
||||
DecryptedParameters,
|
||||
KeySystemKeyManagerInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypeUsesKeySystemRootKeyEncryption,
|
||||
ContentTypesUsingRootKeyEncryption,
|
||||
ContentTypeUsesRootKeyEncryption,
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
@@ -34,25 +39,29 @@ import {
|
||||
EncryptedPayloadInterface,
|
||||
EncryptedTransferPayload,
|
||||
FillItemContentSpecialized,
|
||||
KeySystemRootKeyInterface,
|
||||
ItemContent,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyContentSpecialized,
|
||||
ItemsKeyInterface,
|
||||
NamespacedRootKeyInKeychain,
|
||||
PayloadEmitSource,
|
||||
PayloadTimestampDefaults,
|
||||
RootKeyContent,
|
||||
RootKeyInterface,
|
||||
SureFindPayload,
|
||||
KeySystemIdentifier,
|
||||
} from '@standardnotes/models'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
|
||||
import { DeviceInterface } from '../Device/DeviceInterface'
|
||||
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { StorageKey } from '../Storage/StorageKeys'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
@@ -60,10 +69,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
public memoizedRootKeyParams?: SNRootKeyParams
|
||||
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
public deviceInterface: DeviceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private keys: KeySystemKeyManagerInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
@@ -71,7 +83,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.operatorManager as unknown) = undefined
|
||||
;(this.deviceInterface as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.keys as unknown) = undefined
|
||||
|
||||
this.rootKey = undefined
|
||||
this.memoizedRootKeyParams = undefined
|
||||
super.deinit()
|
||||
@@ -144,7 +162,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
if (this.hasAccount()) {
|
||||
return this.getSureUserVersion()
|
||||
} else if (this.hasPasscode()) {
|
||||
const passcodeParams = await this.getSureRootKeyWrapperKeyParams()
|
||||
const passcodeParams = this.getSureRootKeyWrapperKeyParams()
|
||||
return passcodeParams.version
|
||||
}
|
||||
|
||||
@@ -170,7 +188,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
return undefined
|
||||
}
|
||||
|
||||
const keyParams = await this.getSureRootKeyParams()
|
||||
const keyParams = this.getSureRootKeyParams()
|
||||
|
||||
return CreateNewRootKey({
|
||||
...rawKey,
|
||||
@@ -193,11 +211,8 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
})
|
||||
}
|
||||
|
||||
public async getRootKeyWrapperKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(
|
||||
StorageKey.RootKeyWrapperKeyParams,
|
||||
StorageValueModes.Nonwrapped,
|
||||
)
|
||||
public getRootKeyWrapperKeyParams(): SNRootKeyParams | undefined {
|
||||
const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyWrapperKeyParams, StorageValueModes.Nonwrapped)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return undefined
|
||||
@@ -206,11 +221,11 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
return CreateAnyKeyParams(rawKeyParams as AnyKeyParamsContent)
|
||||
}
|
||||
|
||||
public async getSureRootKeyWrapperKeyParams() {
|
||||
return this.getRootKeyWrapperKeyParams() as Promise<SNRootKeyParams>
|
||||
public getSureRootKeyWrapperKeyParams() {
|
||||
return this.getRootKeyWrapperKeyParams() as SNRootKeyParams
|
||||
}
|
||||
|
||||
public async getRootKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
public getRootKeyParams(): SNRootKeyParams | undefined {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.getRootKeyWrapperKeyParams()
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
@@ -222,22 +237,22 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
}
|
||||
}
|
||||
|
||||
public async getSureRootKeyParams(): Promise<SNRootKeyParams> {
|
||||
return this.getRootKeyParams() as Promise<SNRootKeyParams>
|
||||
public getSureRootKeyParams(): SNRootKeyParams {
|
||||
return this.getRootKeyParams() as SNRootKeyParams
|
||||
}
|
||||
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
|
||||
public async computeRootKey<K extends RootKeyInterface>(password: string, keyParams: SNRootKeyParams): Promise<K> {
|
||||
const version = keyParams.version
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
return operator.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
public async createRootKey(
|
||||
public async createRootKey<K extends RootKeyInterface>(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
version?: ProtocolVersion,
|
||||
) {
|
||||
): Promise<K> {
|
||||
const operator = version ? this.operatorManager.operatorForVersion(version) : this.operatorManager.defaultOperator()
|
||||
return operator.createRootKey(identifier, password, origination)
|
||||
}
|
||||
@@ -291,8 +306,8 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
}
|
||||
}
|
||||
|
||||
private async recomputeAccountKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
|
||||
private recomputeAccountKeyParams(): SNRootKeyParams | undefined {
|
||||
const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return
|
||||
@@ -308,10 +323,12 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
*/
|
||||
private async wrapAndPersistRootKey(wrappingKey: SNRootKey) {
|
||||
const rootKey = this.getSureRootKey()
|
||||
|
||||
const value: DecryptedTransferPayload = {
|
||||
...rootKey.payload.ejected(),
|
||||
content: FillItemContentSpecialized(rootKey.persistableValueWhenWrapping()),
|
||||
}
|
||||
|
||||
const payload = new DecryptedPayload(value)
|
||||
|
||||
const wrappedKey = await this.encryptPayload(payload, wrappingKey)
|
||||
@@ -371,7 +388,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
await this.reencryptItemsKeys()
|
||||
await this.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
} else {
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
@@ -487,35 +504,65 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
}
|
||||
|
||||
private getItemsKeys() {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
return this.items.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public async encrypPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.getRootKey()
|
||||
private async encryptPayloadWithKeyLookup(
|
||||
payload: DecryptedPayloadInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters> {
|
||||
let key: RootKeyInterface | KeySystemRootKeyInterface | undefined
|
||||
if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) {
|
||||
if (!payload.key_system_identifier) {
|
||||
throw Error(`Key system-encrypted payload ${payload.content_type}is missing a key_system_identifier`)
|
||||
}
|
||||
key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier)
|
||||
} else {
|
||||
key = this.getRootKey()
|
||||
}
|
||||
|
||||
if (key == undefined) {
|
||||
throw Error('Attempting root key encryption with no root key')
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
return this.encryptPayload(payload, key, signingKeyPair)
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload)))
|
||||
public async encryptPayloadsWithKeyLookup(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair)))
|
||||
}
|
||||
|
||||
public async encryptPayload(payload: DecryptedPayloadInterface, key: RootKeyInterface): Promise<EncryptedParameters> {
|
||||
return encryptPayload(payload, key, this.operatorManager)
|
||||
public async encryptPayload(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: RootKeyInterface | KeySystemRootKeyInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
): Promise<EncryptedOutputParameters> {
|
||||
return encryptPayload(payload, key, this.operatorManager, signingKeyPair)
|
||||
}
|
||||
|
||||
public async encryptPayloads(payloads: DecryptedPayloadInterface[], key: RootKeyInterface) {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
public async encryptPayloads(
|
||||
payloads: DecryptedPayloadInterface[],
|
||||
key: RootKeyInterface | KeySystemRootKeyInterface,
|
||||
signingKeyPair?: PkcKeyPair,
|
||||
) {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const key = this.getRootKey()
|
||||
let key: RootKeyInterface | KeySystemRootKeyInterface | undefined
|
||||
if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) {
|
||||
if (!payload.key_system_identifier) {
|
||||
throw Error('Key system root key encrypted payload is missing key_system_identifier')
|
||||
}
|
||||
key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier)
|
||||
} else {
|
||||
key = this.getRootKey()
|
||||
}
|
||||
|
||||
if (key == undefined) {
|
||||
return {
|
||||
@@ -530,7 +577,7 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
|
||||
public async decryptPayload<C extends ItemContent = ItemContent>(
|
||||
payload: EncryptedPayloadInterface,
|
||||
key: RootKeyInterface,
|
||||
key: RootKeyInterface | KeySystemRootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
return decryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
@@ -543,25 +590,63 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
|
||||
public async decryptPayloads<C extends ItemContent = ItemContent>(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
key: RootKeyInterface,
|
||||
key: RootKeyInterface | KeySystemRootKeyInterface,
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key changes (non-null only), we must re-encrypt all items
|
||||
* keys with this new root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
public async decryptErroredRootPayloads(): Promise<void> {
|
||||
const erroredRootPayloads = this.payloadManager.invalidPayloads.filter(
|
||||
(i) =>
|
||||
ContentTypeUsesRootKeyEncryption(i.content_type) || ContentTypeUsesKeySystemRootKeyEncryption(i.content_type),
|
||||
)
|
||||
if (erroredRootPayloads.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (itemsKeys.length > 0) {
|
||||
const resultParams = await this.decryptPayloadsWithKeyLookup(erroredRootPayloads)
|
||||
|
||||
const decryptedPayloads = resultParams.map((params) => {
|
||||
const original = SureFindPayload(erroredRootPayloads, params.uuid)
|
||||
if (isErrorDecryptingParameters(params)) {
|
||||
return new EncryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
} else {
|
||||
return new DecryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
||||
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
|
||||
if (items.length > 0) {
|
||||
/**
|
||||
* Do not call sync after marking dirty.
|
||||
* Re-encrypting items keys is called by consumers who have specific flows who
|
||||
* will sync on their own timing
|
||||
*/
|
||||
await this.itemManager.setItemsDirty(itemsKeys)
|
||||
await this.mutator.setItemsDirty(items)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,14 +684,13 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
})
|
||||
|
||||
for (const key of defaultKeys) {
|
||||
await this.itemManager.changeItemsKey(key, (mutator) => {
|
||||
await this.mutator.changeItemsKey(key, (mutator) => {
|
||||
mutator.isDefault = false
|
||||
})
|
||||
}
|
||||
|
||||
const itemsKey = (await this.itemManager.insertItem(itemTemplate)) as ItemsKeyInterface
|
||||
|
||||
await this.itemManager.changeItemsKey(itemsKey, (mutator) => {
|
||||
const itemsKey = await this.mutator.insertItem<ItemsKeyInterface>(itemTemplate)
|
||||
await this.mutator.changeItemsKey(itemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
|
||||
@@ -618,10 +702,10 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
const newDefaultItemsKey = await this.createNewDefaultItemsKey()
|
||||
|
||||
const rollback = async () => {
|
||||
await this.itemManager.setItemToBeDeleted(newDefaultItemsKey)
|
||||
await this.mutator.setItemToBeDeleted(newDefaultItemsKey)
|
||||
|
||||
if (currentDefaultItemsKey) {
|
||||
await this.itemManager.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||
await this.mutator.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
}
|
||||
@@ -629,19 +713,4 @@ export class RootKeyEncryptionService extends AbstractService<RootKeyServiceEven
|
||||
|
||||
return rollback
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return {
|
||||
rootKeyEncryption: {
|
||||
hasRootKey: this.rootKey != undefined,
|
||||
keyMode: KeyMode[this.keyMode],
|
||||
hasRootKeyWrapper: await this.hasRootKeyWrapper(),
|
||||
hasAccount: this.hasAccount(),
|
||||
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
|
||||
hasPasscode: this.hasPasscode(),
|
||||
getEncryptionSourceVersion: this.hasRootKeyEncryptionSource() && (await this.getEncryptionSourceVersion()),
|
||||
getUserVersion: this.getUserVersion(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,79 @@
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
export enum ApplicationEvent {
|
||||
SignedIn = 2,
|
||||
SignedOut = 3,
|
||||
SignedIn = 'signed-in',
|
||||
SignedOut = 'signed-out',
|
||||
|
||||
/** When a full, potentially multi-page sync completes */
|
||||
CompletedFullSync = 5,
|
||||
CompletedFullSync = 'completed-full-sync',
|
||||
|
||||
FailedSync = 6,
|
||||
HighLatencySync = 7,
|
||||
EnteredOutOfSync = 8,
|
||||
ExitedOutOfSync = 9,
|
||||
FailedSync = 'failed-sync',
|
||||
HighLatencySync = 'high-latency-sync',
|
||||
EnteredOutOfSync = 'entered-out-of-sync',
|
||||
ExitedOutOfSync = 'exited-out-of-sync',
|
||||
|
||||
ApplicationStageChanged = 'application-stage-changed',
|
||||
|
||||
/**
|
||||
* The application has finished it `prepareForLaunch` state and is now ready for unlock
|
||||
* The application has finished its prepareForLaunch state and is now ready for unlock
|
||||
* Called when the application has initialized and is ready for launch, but before
|
||||
* the application has been unlocked, if applicable. Use this to do pre-launch
|
||||
* configuration, but do not attempt to access user data like notes or tags.
|
||||
*/
|
||||
Started = 10,
|
||||
Started = 'started',
|
||||
|
||||
/**
|
||||
* The applicaiton is fully unlocked and ready for i/o
|
||||
* Called when the application has been fully decrypted and unlocked. Use this to
|
||||
* to begin streaming data like notes and tags.
|
||||
*/
|
||||
Launched = 11,
|
||||
LocalDataLoaded = 12,
|
||||
Launched = 'launched',
|
||||
|
||||
LocalDataLoaded = 'local-data-loaded',
|
||||
|
||||
/**
|
||||
* When the root key or root key wrapper changes. Includes events like account state
|
||||
* changes (registering, signing in, changing pw, logging out) and passcode state
|
||||
* changes (adding, removing, changing).
|
||||
*/
|
||||
KeyStatusChanged = 13,
|
||||
KeyStatusChanged = 'key-status-changed',
|
||||
|
||||
MajorDataChange = 14,
|
||||
CompletedRestart = 15,
|
||||
LocalDataIncrementalLoad = 16,
|
||||
SyncStatusChanged = 17,
|
||||
WillSync = 18,
|
||||
InvalidSyncSession = 19,
|
||||
LocalDatabaseReadError = 20,
|
||||
LocalDatabaseWriteError = 21,
|
||||
MajorDataChange = 'major-data-change',
|
||||
CompletedRestart = 'completed-restart',
|
||||
LocalDataIncrementalLoad = 'local-data-incremental-load',
|
||||
SyncStatusChanged = 'sync-status-changed',
|
||||
WillSync = 'will-sync',
|
||||
InvalidSyncSession = 'invalid-sync-session',
|
||||
LocalDatabaseReadError = 'local-database-read-error',
|
||||
LocalDatabaseWriteError = 'local-database-write-error',
|
||||
|
||||
/** When a single roundtrip completes with sync, in a potentially multi-page sync request.
|
||||
* If just a single roundtrip, this event will be triggered, along with CompletedFullSync */
|
||||
CompletedIncrementalSync = 22,
|
||||
/**
|
||||
* When a single roundtrip completes with sync, in a potentially multi-page sync request.
|
||||
* If just a single roundtrip, this event will be triggered, along with CompletedFullSync
|
||||
*/
|
||||
CompletedIncrementalSync = 'completed-incremental-sync',
|
||||
|
||||
/**
|
||||
* The application has loaded all pending migrations (but not run any, except for the base one),
|
||||
* and consumers may now call `hasPendingMigrations`
|
||||
* and consumers may now call hasPendingMigrations
|
||||
*/
|
||||
MigrationsLoaded = 23,
|
||||
MigrationsLoaded = 'migrations-loaded',
|
||||
|
||||
/** When StorageService is ready to start servicing read/write requests */
|
||||
StorageReady = 24,
|
||||
/** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */
|
||||
StorageReady = 'storage-ready',
|
||||
|
||||
PreferencesChanged = 'preferences-changed',
|
||||
UnprotectedSessionBegan = 'unprotected-session-began',
|
||||
UserRolesChanged = 'user-roles-changed',
|
||||
FeaturesUpdated = 'features-updated',
|
||||
UnprotectedSessionExpired = 'unprotected-session-expired',
|
||||
|
||||
PreferencesChanged = 25,
|
||||
UnprotectedSessionBegan = 26,
|
||||
UserRolesChanged = 27,
|
||||
FeaturesUpdated = 28,
|
||||
UnprotectedSessionExpired = 29,
|
||||
/** Called when the app first launches and after first sync request made after sign in */
|
||||
CompletedInitialSync = 30,
|
||||
BiometricsSoftLockEngaged = 31,
|
||||
BiometricsSoftLockDisengaged = 32,
|
||||
DidPurchaseSubscription = 33,
|
||||
CompletedInitialSync = 'completed-initial-sync',
|
||||
BiometricsSoftLockEngaged = 'biometrics-soft-lock-engaged',
|
||||
BiometricsSoftLockDisengaged = 'biometrics-soft-lock-disengaged',
|
||||
DidPurchaseSubscription = 'did-purchase-subscription',
|
||||
}
|
||||
|
||||
export type ApplicationStageChangedEventPayload = {
|
||||
stage: ApplicationStage
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
AsymmetricMessageServerHash,
|
||||
SharedVaultInviteServerHash,
|
||||
SharedVaultServerHash,
|
||||
UserEventServerHash,
|
||||
} from '@standardnotes/responses'
|
||||
|
||||
/* istanbul ignore file */
|
||||
export enum SyncEvent {
|
||||
/**
|
||||
@@ -7,8 +14,8 @@ export enum SyncEvent {
|
||||
*/
|
||||
SyncCompletedWithAllItemsUploaded = 'SyncCompletedWithAllItemsUploaded',
|
||||
SyncCompletedWithAllItemsUploadedAndDownloaded = 'SyncCompletedWithAllItemsUploadedAndDownloaded',
|
||||
SingleRoundTripSyncCompleted = 'SingleRoundTripSyncCompleted',
|
||||
SyncWillBegin = 'sync:will-begin',
|
||||
PaginatedSyncRequestCompleted = 'PaginatedSyncRequestCompleted',
|
||||
SyncDidBeginProcessing = 'sync:did-begin-processing',
|
||||
DownloadFirstSyncCompleted = 'sync:download-first-completed',
|
||||
SyncTakingTooLong = 'sync:taking-too-long',
|
||||
SyncError = 'sync:error',
|
||||
@@ -22,4 +29,13 @@ export enum SyncEvent {
|
||||
DatabaseWriteError = 'database-write-error',
|
||||
DatabaseReadError = 'database-read-error',
|
||||
SyncRequestsIntegrityCheck = 'sync:requests-integrity-check',
|
||||
ReceivedRemoteSharedVaults = 'received-shared-vaults',
|
||||
ReceivedSharedVaultInvites = 'received-shared-vault-invites',
|
||||
ReceivedUserEvents = 'received-user-events',
|
||||
ReceivedAsymmetricMessages = 'received-asymmetric-messages',
|
||||
}
|
||||
|
||||
export type SyncEventReceivedRemoteSharedVaultsData = SharedVaultServerHash[]
|
||||
export type SyncEventReceivedSharedVaultInvitesData = SharedVaultInviteServerHash[]
|
||||
export type SyncEventReceivedAsymmetricMessagesData = AsymmetricMessageServerHash[]
|
||||
export type SyncEventReceivedUserEventsData = UserEventServerHash[]
|
||||
|
||||
@@ -3,16 +3,18 @@ import { FileItem } from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { ChallengeServiceInterface } from '../Challenge'
|
||||
import { InternalEventBusInterface } from '..'
|
||||
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 itemManager: ItemManagerInterface
|
||||
let mutator: MutatorClientInterface
|
||||
let syncService: SyncServiceInterface
|
||||
let alertService: AlertService
|
||||
let crypto: PureCryptoInterface
|
||||
@@ -21,26 +23,28 @@ describe('fileService', () => {
|
||||
let encryptor: EncryptionProviderInterface
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let backupService: BackupServiceInterface
|
||||
let http: HttpServiceInterface
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<ApiServiceInterface>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
apiService.createFileValetToken = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
apiService.deleteFile = jest.fn().mockReturnValue({})
|
||||
const numChunks = 1
|
||||
apiService.downloadFile = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
_file: string,
|
||||
_chunkIndex: number,
|
||||
_apiToken: string,
|
||||
_rangeStart: number,
|
||||
onBytesReceived: (bytes: Uint8Array) => void,
|
||||
) => {
|
||||
(params: {
|
||||
_file: string
|
||||
_chunkIndex: number
|
||||
_apiToken: string
|
||||
_ownershipType: string
|
||||
_rangeStart: number
|
||||
onBytesReceived: (bytes: Uint8Array) => void
|
||||
}) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
for (let i = 0; i < numChunks; i++) {
|
||||
onBytesReceived(Uint8Array.from([0xaa]))
|
||||
params.onBytesReceived(Uint8Array.from([0xaa]))
|
||||
}
|
||||
|
||||
resolve()
|
||||
@@ -49,11 +53,13 @@ describe('fileService', () => {
|
||||
)
|
||||
|
||||
itemManager = {} as jest.Mocked<ItemManagerInterface>
|
||||
itemManager.createItem = jest.fn()
|
||||
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
|
||||
itemManager.setItemToBeDeleted = jest.fn()
|
||||
itemManager.addObserver = jest.fn()
|
||||
itemManager.changeItem = jest.fn()
|
||||
|
||||
mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||
mutator.createItem = jest.fn()
|
||||
mutator.setItemToBeDeleted = jest.fn()
|
||||
mutator.changeItem = jest.fn()
|
||||
|
||||
challengor = {} as jest.Mocked<ChallengeServiceInterface>
|
||||
|
||||
@@ -75,12 +81,15 @@ describe('fileService', () => {
|
||||
backupService.readEncryptedFileFromBackup = jest.fn()
|
||||
backupService.getFileBackupInfo = jest.fn()
|
||||
|
||||
http = {} as jest.Mocked<HttpServiceInterface>
|
||||
|
||||
fileService = new FileService(
|
||||
apiService,
|
||||
itemManager,
|
||||
mutator,
|
||||
syncService,
|
||||
encryptor,
|
||||
challengor,
|
||||
http,
|
||||
alertService,
|
||||
crypto,
|
||||
internalEventBus,
|
||||
@@ -152,7 +161,7 @@ describe('fileService', () => {
|
||||
} as jest.Mocked<FileItem>
|
||||
|
||||
const alertMock = (alertService.confirm = jest.fn().mockReturnValue(true))
|
||||
const deleteItemMock = (itemManager.setItemToBeDeleted = jest.fn())
|
||||
const deleteItemMock = (mutator.setItemToBeDeleted = jest.fn())
|
||||
|
||||
apiService.deleteFile = jest.fn().mockReturnValue({ data: { error: true } })
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
ValetTokenOperation,
|
||||
isClientDisplayableError,
|
||||
isErrorResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
FileItem,
|
||||
@@ -9,6 +15,8 @@ import {
|
||||
FileContent,
|
||||
EncryptedPayload,
|
||||
isEncryptedPayload,
|
||||
VaultListingInterface,
|
||||
SharedVaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
|
||||
@@ -36,29 +44,37 @@ import {
|
||||
import { AlertService, ButtonType } from '../Alert/AlertService'
|
||||
import { ChallengeServiceInterface } from '../Challenge'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
|
||||
import { log, LoggingDomain } from '../Logging'
|
||||
import {
|
||||
SharedVaultMoveType,
|
||||
SharedVaultServer,
|
||||
SharedVaultServerInterface,
|
||||
HttpServiceInterface,
|
||||
} from '@standardnotes/api'
|
||||
|
||||
const OneHundredMb = 100 * 1_000_000
|
||||
|
||||
export class FileService extends AbstractService implements FilesClientInterface {
|
||||
private encryptedCache: FileMemoryCache = new FileMemoryCache(OneHundredMb)
|
||||
private sharedVault: SharedVaultServerInterface
|
||||
|
||||
constructor(
|
||||
private api: FilesApiInterface,
|
||||
private itemManager: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private syncService: SyncServiceInterface,
|
||||
private encryptor: EncryptionProviderInterface,
|
||||
private challengor: ChallengeServiceInterface,
|
||||
http: HttpServiceInterface,
|
||||
private alertService: AlertService,
|
||||
private crypto: PureCryptoInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
private backupsService?: BackupServiceInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
this.sharedVault = new SharedVaultServer(http)
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
@@ -67,7 +83,6 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
this.encryptedCache.clear()
|
||||
;(this.encryptedCache as unknown) = undefined
|
||||
;(this.api as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.encryptor as unknown) = undefined
|
||||
;(this.syncService as unknown) = undefined
|
||||
;(this.alertService as unknown) = undefined
|
||||
@@ -79,14 +94,109 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
return 5_000_000
|
||||
}
|
||||
|
||||
private async createUserValetToken(
|
||||
remoteIdentifier: string,
|
||||
operation: ValetTokenOperation,
|
||||
unencryptedFileSizeForUpload?: number | undefined,
|
||||
): Promise<string | ClientDisplayableError> {
|
||||
return this.api.createUserFileValetToken(remoteIdentifier, operation, unencryptedFileSizeForUpload)
|
||||
}
|
||||
|
||||
private async createSharedVaultValetToken(params: {
|
||||
sharedVaultUuid: string
|
||||
remoteIdentifier: string
|
||||
operation: ValetTokenOperation
|
||||
fileUuidRequiredForExistingFiles?: string
|
||||
unencryptedFileSizeForUpload?: number | undefined
|
||||
moveOperationType?: SharedVaultMoveType
|
||||
sharedVaultToSharedVaultMoveTargetUuid?: string
|
||||
}): Promise<string | ClientDisplayableError> {
|
||||
if (params.operation !== 'write' && !params.fileUuidRequiredForExistingFiles) {
|
||||
throw new Error('File UUID is required for for non-write operations')
|
||||
}
|
||||
|
||||
const valetTokenResponse = await this.sharedVault.createSharedVaultFileValetToken({
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
fileUuid: params.fileUuidRequiredForExistingFiles,
|
||||
remoteIdentifier: params.remoteIdentifier,
|
||||
operation: params.operation,
|
||||
unencryptedFileSize: params.unencryptedFileSizeForUpload,
|
||||
moveOperationType: params.moveOperationType,
|
||||
sharedVaultToSharedVaultMoveTargetUuid: params.sharedVaultToSharedVaultMoveTargetUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(valetTokenResponse)) {
|
||||
return new ClientDisplayableError('Could not create valet token')
|
||||
}
|
||||
|
||||
return valetTokenResponse.data.valetToken
|
||||
}
|
||||
|
||||
public async moveFileToSharedVault(
|
||||
file: FileItem,
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
): Promise<void | ClientDisplayableError> {
|
||||
const valetTokenResult = await this.createSharedVaultValetToken({
|
||||
sharedVaultUuid: file.shared_vault_uuid ? file.shared_vault_uuid : sharedVault.sharing.sharedVaultUuid,
|
||||
remoteIdentifier: file.remoteIdentifier,
|
||||
operation: 'move',
|
||||
fileUuidRequiredForExistingFiles: file.uuid,
|
||||
moveOperationType: file.shared_vault_uuid ? 'shared-vault-to-shared-vault' : 'user-to-shared-vault',
|
||||
sharedVaultToSharedVaultMoveTargetUuid: file.shared_vault_uuid ? sharedVault.sharing.sharedVaultUuid : undefined,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(valetTokenResult)) {
|
||||
return valetTokenResult
|
||||
}
|
||||
|
||||
const moveResult = await this.api.moveFile(valetTokenResult)
|
||||
|
||||
if (!moveResult) {
|
||||
return new ClientDisplayableError('Could not move file')
|
||||
}
|
||||
}
|
||||
|
||||
public async moveFileOutOfSharedVault(file: FileItem): Promise<void | ClientDisplayableError> {
|
||||
if (!file.shared_vault_uuid) {
|
||||
return new ClientDisplayableError('File is not in a shared vault')
|
||||
}
|
||||
|
||||
const valetTokenResult = await this.createSharedVaultValetToken({
|
||||
sharedVaultUuid: file.shared_vault_uuid,
|
||||
remoteIdentifier: file.remoteIdentifier,
|
||||
operation: 'move',
|
||||
fileUuidRequiredForExistingFiles: file.uuid,
|
||||
moveOperationType: 'shared-vault-to-user',
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(valetTokenResult)) {
|
||||
return valetTokenResult
|
||||
}
|
||||
|
||||
const moveResult = await this.api.moveFile(valetTokenResult)
|
||||
|
||||
if (!moveResult) {
|
||||
return new ClientDisplayableError('Could not move file')
|
||||
}
|
||||
}
|
||||
|
||||
public async beginNewFileUpload(
|
||||
sizeInBytes: number,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<EncryptAndUploadFileOperation | ClientDisplayableError> {
|
||||
const remoteIdentifier = UuidGenerator.GenerateUuid()
|
||||
const tokenResult = await this.api.createFileValetToken(remoteIdentifier, 'write', sizeInBytes)
|
||||
const valetTokenResult =
|
||||
vault && vault.isSharedVaultListing()
|
||||
? await this.createSharedVaultValetToken({
|
||||
sharedVaultUuid: vault.sharing.sharedVaultUuid,
|
||||
remoteIdentifier,
|
||||
operation: 'write',
|
||||
unencryptedFileSizeForUpload: sizeInBytes,
|
||||
})
|
||||
: await this.createUserValetToken(remoteIdentifier, 'write', sizeInBytes)
|
||||
|
||||
if (tokenResult instanceof ClientDisplayableError) {
|
||||
return tokenResult
|
||||
if (valetTokenResult instanceof ClientDisplayableError) {
|
||||
return valetTokenResult
|
||||
}
|
||||
|
||||
const key = this.crypto.generateRandomKey(FileProtocolV1Constants.KeySize)
|
||||
@@ -97,9 +207,18 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
decryptedSize: sizeInBytes,
|
||||
}
|
||||
|
||||
const uploadOperation = new EncryptAndUploadFileOperation(fileParams, tokenResult, this.crypto, this.api)
|
||||
const uploadOperation = new EncryptAndUploadFileOperation(
|
||||
fileParams,
|
||||
valetTokenResult,
|
||||
this.crypto,
|
||||
this.api,
|
||||
vault,
|
||||
)
|
||||
|
||||
const uploadSessionStarted = await this.api.startUploadSession(tokenResult)
|
||||
const uploadSessionStarted = await this.api.startUploadSession(
|
||||
valetTokenResult,
|
||||
vault && vault.isSharedVaultListing() ? 'shared-vault' : 'user',
|
||||
)
|
||||
|
||||
if (isErrorResponse(uploadSessionStarted) || !uploadSessionStarted.data.uploadId) {
|
||||
return new ClientDisplayableError('Could not start upload session')
|
||||
@@ -127,7 +246,10 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
operation: EncryptAndUploadFileOperation,
|
||||
fileMetadata: FileMetadata,
|
||||
): Promise<FileItem | ClientDisplayableError> {
|
||||
const uploadSessionClosed = await this.api.closeUploadSession(operation.getApiToken())
|
||||
const uploadSessionClosed = await this.api.closeUploadSession(
|
||||
operation.getValetToken(),
|
||||
operation.vault && operation.vault.isSharedVaultListing() ? 'shared-vault' : 'user',
|
||||
)
|
||||
|
||||
if (!uploadSessionClosed) {
|
||||
return new ClientDisplayableError('Could not close upload session')
|
||||
@@ -145,10 +267,11 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
remoteIdentifier: result.remoteIdentifier,
|
||||
}
|
||||
|
||||
const file = await this.itemManager.createItem<FileItem>(
|
||||
const file = await this.mutator.createItem<FileItem>(
|
||||
ContentType.File,
|
||||
FillItemContentSpecialized(fileContent),
|
||||
true,
|
||||
operation.vault,
|
||||
)
|
||||
|
||||
await this.syncService.sync()
|
||||
@@ -215,7 +338,20 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
|
||||
let cacheEntryAggregate = new Uint8Array()
|
||||
|
||||
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api)
|
||||
const tokenResult = file.shared_vault_uuid
|
||||
? await this.createSharedVaultValetToken({
|
||||
sharedVaultUuid: file.shared_vault_uuid,
|
||||
remoteIdentifier: file.remoteIdentifier,
|
||||
operation: 'read',
|
||||
fileUuidRequiredForExistingFiles: file.uuid,
|
||||
})
|
||||
: await this.createUserValetToken(file.remoteIdentifier, 'read')
|
||||
|
||||
if (tokenResult instanceof ClientDisplayableError) {
|
||||
return tokenResult
|
||||
}
|
||||
|
||||
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api, tokenResult)
|
||||
|
||||
const result = await operation.run(async ({ decrypted, encrypted, progress }): Promise<void> => {
|
||||
if (addToCache) {
|
||||
@@ -235,13 +371,20 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
public async deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined> {
|
||||
this.encryptedCache.remove(file.uuid)
|
||||
|
||||
const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'delete')
|
||||
const tokenResult = file.shared_vault_uuid
|
||||
? await this.createSharedVaultValetToken({
|
||||
sharedVaultUuid: file.shared_vault_uuid,
|
||||
remoteIdentifier: file.remoteIdentifier,
|
||||
operation: 'delete',
|
||||
fileUuidRequiredForExistingFiles: file.uuid,
|
||||
})
|
||||
: await this.createUserValetToken(file.remoteIdentifier, 'delete')
|
||||
|
||||
if (tokenResult instanceof ClientDisplayableError) {
|
||||
return tokenResult
|
||||
}
|
||||
|
||||
const result = await this.api.deleteFile(tokenResult)
|
||||
const result = await this.api.deleteFile(tokenResult, file.shared_vault_uuid ? 'shared-vault' : 'user')
|
||||
|
||||
if (result.data?.error) {
|
||||
const deleteAnyway = await this.alertService.confirm(
|
||||
@@ -261,7 +404,7 @@ export class FileService extends AbstractService implements FilesClientInterface
|
||||
}
|
||||
}
|
||||
|
||||
await this.itemManager.setItemToBeDeleted(file)
|
||||
await this.mutator.setItemToBeDeleted(file)
|
||||
await this.syncService.sync()
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum InternalFeature {
|
||||
Vaults = 'vaults',
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InternalFeature } from './InternalFeature'
|
||||
import { InternalFeatureServiceInterface } from './InternalFeatureServiceInterface'
|
||||
|
||||
let sharedInstance: InternalFeatureServiceInterface | undefined
|
||||
|
||||
export class InternalFeatureService implements InternalFeatureServiceInterface {
|
||||
static get(): InternalFeatureServiceInterface {
|
||||
if (!sharedInstance) {
|
||||
sharedInstance = new InternalFeatureService()
|
||||
}
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
private readonly enabledFeatures: Set<InternalFeature> = new Set()
|
||||
|
||||
isFeatureEnabled(feature: InternalFeature): boolean {
|
||||
return this.enabledFeatures.has(feature)
|
||||
}
|
||||
|
||||
enableFeature(feature: InternalFeature): void {
|
||||
console.warn(`Enabling internal feature: ${feature}`)
|
||||
this.enabledFeatures.add(feature)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { InternalFeature } from './InternalFeature'
|
||||
|
||||
export interface InternalFeatureServiceInterface {
|
||||
isFeatureEnabled(feature: InternalFeature): boolean
|
||||
enableFeature(feature: InternalFeature): void
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { SNNote, SNTag, ItemCounts } from '@standardnotes/models'
|
||||
|
||||
export interface ItemCounterInterface {
|
||||
countNotesAndTags(items: Array<SNNote | SNTag>): ItemCounts
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
MutationType,
|
||||
ItemsKeyInterface,
|
||||
ItemsKeyMutatorInterface,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
DecryptedPayloadInterface,
|
||||
PayloadEmitSource,
|
||||
EncryptedItemInterface,
|
||||
DeletedItemInterface,
|
||||
@@ -13,6 +9,20 @@ import {
|
||||
PredicateInterface,
|
||||
DecryptedPayload,
|
||||
SNTag,
|
||||
ItemInterface,
|
||||
AnyItemInterface,
|
||||
KeySystemIdentifier,
|
||||
ItemCollection,
|
||||
SNNote,
|
||||
SmartView,
|
||||
TagItemCountChangeObserver,
|
||||
SNComponent,
|
||||
SNTheme,
|
||||
DecryptedPayloadInterface,
|
||||
DecryptedTransferPayload,
|
||||
FileItem,
|
||||
VaultDisplayOptions,
|
||||
NotesAndFilesDisplayControllerOptions,
|
||||
} from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
|
||||
@@ -41,26 +51,20 @@ export type ItemManagerChangeObserverCallback<I extends DecryptedItemInterface =
|
||||
) => void
|
||||
|
||||
export interface ItemManagerInterface extends AbstractService {
|
||||
getCollection(): ItemCollection
|
||||
|
||||
addObserver<I extends DecryptedItemInterface = DecryptedItemInterface>(
|
||||
contentType: ContentType | ContentType[],
|
||||
callback: ItemManagerChangeObserverCallback<I>,
|
||||
): () => void
|
||||
setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise<void>
|
||||
setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise<void>
|
||||
setItemsDirty(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
isUserModified?: boolean,
|
||||
): Promise<DecryptedItemInterface[]>
|
||||
|
||||
get items(): DecryptedItemInterface[]
|
||||
insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
|
||||
emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
|
||||
|
||||
getItems<T extends DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[]
|
||||
get invalidItems(): EncryptedItemInterface[]
|
||||
allTrackedItems(): ItemInterface[]
|
||||
getDisplayableItemsKeys(): ItemsKeyInterface[]
|
||||
createItem<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
|
||||
contentType: ContentType,
|
||||
content: C,
|
||||
needsSync?: boolean,
|
||||
): Promise<T>
|
||||
|
||||
createTemplateItem<
|
||||
C extends ItemContent = ItemContent,
|
||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
@@ -69,23 +73,7 @@ export interface ItemManagerInterface extends AbstractService {
|
||||
content?: C,
|
||||
override?: Partial<DecryptedPayload<C>>,
|
||||
): I
|
||||
changeItem<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemToLookupUuidFor: I,
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I>
|
||||
changeItemsKey(
|
||||
itemToLookupUuidFor: ItemsKeyInterface,
|
||||
mutate: (mutator: ItemsKeyMutatorInterface) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<ItemsKeyInterface>
|
||||
|
||||
itemsMatchingPredicate<T extends DecryptedItemInterface>(
|
||||
contentType: ContentType,
|
||||
predicate: PredicateInterface<T>,
|
||||
@@ -96,12 +84,47 @@ export interface ItemManagerInterface extends AbstractService {
|
||||
): T[]
|
||||
subItemsMatchingPredicates<T extends DecryptedItemInterface>(items: T[], predicates: PredicateInterface<T>[]): T[]
|
||||
removeAllItemsFromMemory(): Promise<void>
|
||||
removeItemsLocally(items: AnyItemInterface[]): void
|
||||
getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[]
|
||||
getTagLongTitle(tag: SNTag): string
|
||||
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
|
||||
itemsReferencingItem<I extends DecryptedItemInterface = DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: { uuid: string },
|
||||
contentType?: ContentType,
|
||||
): I[]
|
||||
referencesForItem<I extends DecryptedItemInterface = DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
contentType?: ContentType,
|
||||
): I[]
|
||||
findItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T | undefined
|
||||
findItems<T extends DecryptedItemInterface>(uuids: string[]): T[]
|
||||
findSureItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T
|
||||
get trashedItems(): SNNote[]
|
||||
itemsBelongingToKeySystem(systemIdentifier: KeySystemIdentifier): DecryptedItemInterface[]
|
||||
hasTagsNeedingFoldersMigration(): boolean
|
||||
get invalidNonVaultedItems(): EncryptedItemInterface[]
|
||||
isTemplateItem(item: DecryptedItemInterface): boolean
|
||||
getSmartViews(): SmartView[]
|
||||
addNoteCountChangeObserver(observer: TagItemCountChangeObserver): () => void
|
||||
allCountableNotesCount(): number
|
||||
allCountableFilesCount(): number
|
||||
countableNotesForTag(tag: SNTag | SmartView): number
|
||||
getNoteCount(): number
|
||||
getDisplayableTags(): SNTag[]
|
||||
getTagChildren(itemToLookupUuidFor: SNTag): SNTag[]
|
||||
getTagParent(itemToLookupUuidFor: SNTag): SNTag | undefined
|
||||
isValidTagParent(parentTagToLookUpUuidFor: SNTag, childToLookUpUuidFor: SNTag): boolean
|
||||
isSmartViewTitle(title: string): boolean
|
||||
getDisplayableComponents(): (SNComponent | SNTheme)[]
|
||||
createItemFromPayload(payload: DecryptedPayloadInterface): DecryptedItemInterface
|
||||
createPayloadFromObject(object: DecryptedTransferPayload): DecryptedPayloadInterface
|
||||
getDisplayableFiles(): FileItem[]
|
||||
setVaultDisplayOptions(options: VaultDisplayOptions): void
|
||||
numberOfNotesWithConflicts(): number
|
||||
getDisplayableNotes(): SNNote[]
|
||||
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
||||
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
||||
getTagPrefixTitle(tag: SNTag): string | undefined
|
||||
getNoteLinkedFiles(note: SNNote): FileItem[]
|
||||
conflictsOf(uuid: string): AnyItemInterface[]
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
SNNote,
|
||||
FileItem,
|
||||
SNTag,
|
||||
SmartView,
|
||||
TagItemCountChangeObserver,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedItemInterface,
|
||||
DecryptedTransferPayload,
|
||||
PredicateInterface,
|
||||
DecryptedItemInterface,
|
||||
SNComponent,
|
||||
SNTheme,
|
||||
DisplayOptions,
|
||||
ItemsKeyInterface,
|
||||
ItemContent,
|
||||
DecryptedPayload,
|
||||
AnyItemInterface,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export interface ItemsClientInterface {
|
||||
get invalidItems(): EncryptedItemInterface[]
|
||||
|
||||
associateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem>
|
||||
|
||||
disassociateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem>
|
||||
|
||||
renameFile(file: FileItem, name: string): Promise<FileItem>
|
||||
|
||||
addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise<SNTag[]>
|
||||
|
||||
addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise<SNTag[]>
|
||||
|
||||
/** Creates an unmanaged, un-inserted item from a payload. */
|
||||
createItemFromPayload(payload: DecryptedPayloadInterface): DecryptedItemInterface
|
||||
|
||||
createPayloadFromObject(object: DecryptedTransferPayload): DecryptedPayloadInterface
|
||||
|
||||
createTemplateItem<
|
||||
C extends ItemContent = ItemContent,
|
||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
contentType: ContentType,
|
||||
content?: C,
|
||||
override?: Partial<DecryptedPayload<C>>,
|
||||
): I
|
||||
|
||||
get trashedItems(): SNNote[]
|
||||
|
||||
setPrimaryItemDisplayOptions(options: DisplayOptions): void
|
||||
|
||||
getDisplayableNotes(): SNNote[]
|
||||
|
||||
getDisplayableTags(): SNTag[]
|
||||
|
||||
getDisplayableItemsKeys(): ItemsKeyInterface[]
|
||||
|
||||
getDisplayableFiles(): FileItem[]
|
||||
|
||||
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
||||
|
||||
getDisplayableComponents(): (SNComponent | SNTheme)[]
|
||||
|
||||
getItems<T extends DecryptedItemInterface>(contentType: ContentType | ContentType[]): T[]
|
||||
|
||||
insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
|
||||
|
||||
notesMatchingSmartView(view: SmartView): SNNote[]
|
||||
|
||||
addNoteCountChangeObserver(observer: TagItemCountChangeObserver): () => void
|
||||
|
||||
allCountableNotesCount(): number
|
||||
allCountableFilesCount(): number
|
||||
|
||||
countableNotesForTag(tag: SNTag | SmartView): number
|
||||
|
||||
findTagByTitle(title: string): SNTag | undefined
|
||||
|
||||
getTagPrefixTitle(tag: SNTag): string | undefined
|
||||
|
||||
getTagLongTitle(tag: SNTag): string
|
||||
|
||||
hasTagsNeedingFoldersMigration(): boolean
|
||||
|
||||
referencesForItem(itemToLookupUuidFor: DecryptedItemInterface, contentType?: ContentType): DecryptedItemInterface[]
|
||||
|
||||
itemsReferencingItem(itemToLookupUuidFor: DecryptedItemInterface, contentType?: ContentType): DecryptedItemInterface[]
|
||||
|
||||
linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote>
|
||||
linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem>
|
||||
|
||||
unlinkItems(
|
||||
itemOne: DecryptedItemInterface<ItemContent>,
|
||||
itemTwo: DecryptedItemInterface<ItemContent>,
|
||||
): Promise<DecryptedItemInterface<ItemContent>>
|
||||
|
||||
/**
|
||||
* Finds tags with title or component starting with a search query and (optionally) not associated with a note
|
||||
* @param searchQuery - The query string to match
|
||||
* @param note - The note whose tags should be omitted from results
|
||||
* @returns Array containing tags matching search query and not associated with note
|
||||
*/
|
||||
searchTags(searchQuery: string, note?: SNNote): SNTag[]
|
||||
|
||||
isValidTagParent(parentTagToLookUpUuidFor: SNTag, childToLookUpUuidFor: SNTag): boolean
|
||||
|
||||
/**
|
||||
* Returns the parent for a tag
|
||||
*/
|
||||
getTagParent(itemToLookupUuidFor: SNTag): SNTag | undefined
|
||||
|
||||
/**
|
||||
* Returns the hierarchy of parents for a tag
|
||||
* @returns Array containing all parent tags
|
||||
*/
|
||||
getTagParentChain(itemToLookupUuidFor: SNTag): SNTag[]
|
||||
|
||||
/**
|
||||
* Returns all descendants for a tag
|
||||
* @returns Array containing all descendant tags
|
||||
*/
|
||||
getTagChildren(itemToLookupUuidFor: SNTag): SNTag[]
|
||||
|
||||
/**
|
||||
* Get tags for a note sorted in natural order
|
||||
* @param item - The item whose tags will be returned
|
||||
* @returns Array containing tags associated with an item
|
||||
*/
|
||||
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
|
||||
|
||||
isSmartViewTitle(title: string): boolean
|
||||
|
||||
getSmartViews(): SmartView[]
|
||||
|
||||
getNoteCount(): number
|
||||
|
||||
/**
|
||||
* Finds an item by UUID.
|
||||
*/
|
||||
findItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T | undefined
|
||||
|
||||
/**
|
||||
* Finds an item by predicate.
|
||||
*/
|
||||
findItems<T extends DecryptedItemInterface>(uuids: string[]): T[]
|
||||
|
||||
findSureItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T
|
||||
|
||||
/**
|
||||
* Finds an item by predicate.
|
||||
*/
|
||||
itemsMatchingPredicate<T extends DecryptedItemInterface>(
|
||||
contentType: ContentType,
|
||||
predicate: PredicateInterface<T>,
|
||||
): T[]
|
||||
|
||||
/**
|
||||
* @param item item to be checked
|
||||
* @returns Whether the item is a template (unmanaged)
|
||||
*/
|
||||
isTemplateItem(item: DecryptedItemInterface): boolean
|
||||
|
||||
createSmartView<T extends DecryptedItemInterface<ItemContent>>(
|
||||
title: string,
|
||||
predicate: PredicateInterface<T>,
|
||||
iconString?: string,
|
||||
): Promise<SmartView>
|
||||
|
||||
conflictsOf(uuid: string): AnyItemInterface[]
|
||||
numberOfNotesWithConflicts(): number
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SNNote, SNTag } from '@standardnotes/models'
|
||||
import { ItemCounter } from './ItemCounter'
|
||||
import { StaticItemCounter } from './StaticItemCounter'
|
||||
|
||||
describe('ItemCounter', () => {
|
||||
const createCounter = () => new ItemCounter()
|
||||
const createCounter = () => new StaticItemCounter()
|
||||
|
||||
it('should count distinct item counts', () => {
|
||||
const items = [
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SNNote, SNTag, ItemCounts } from '@standardnotes/models'
|
||||
|
||||
import { ItemCounterInterface } from './ItemCounterInterface'
|
||||
|
||||
export class ItemCounter implements ItemCounterInterface {
|
||||
export class StaticItemCounter {
|
||||
countNotesAndTags(items: Array<SNNote | SNTag>): ItemCounts {
|
||||
const counts: ItemCounts = {
|
||||
notes: 0,
|
||||
158
packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts
Normal file
158
packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
|
||||
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
DecryptedTransferPayload,
|
||||
EncryptedItemInterface,
|
||||
KeySystemIdentifier,
|
||||
KeySystemItemsKeyInterface,
|
||||
KeySystemRootKey,
|
||||
KeySystemRootKeyContent,
|
||||
KeySystemRootKeyInterface,
|
||||
KeySystemRootKeyStorageMode,
|
||||
Predicate,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ItemManagerInterface } from './../Item/ItemManagerInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { KeySystemKeyManagerInterface } from '@standardnotes/encryption'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
|
||||
const RootKeyStorageKeyPrefix = 'key-system-root-key-'
|
||||
|
||||
export class KeySystemKeyManager extends AbstractService implements KeySystemKeyManagerInterface {
|
||||
private rootKeyMemoryCache: Record<KeySystemIdentifier, KeySystemRootKeyInterface> = {}
|
||||
|
||||
constructor(
|
||||
private readonly items: ItemManagerInterface,
|
||||
private readonly mutator: MutatorClientInterface,
|
||||
private readonly storage: StorageServiceInterface,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
}
|
||||
|
||||
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||
if (stage === ApplicationStage.StorageDecrypted_09) {
|
||||
this.loadRootKeysFromStorage()
|
||||
}
|
||||
}
|
||||
|
||||
private loadRootKeysFromStorage(): void {
|
||||
const storageKeys = this.storage.getAllKeys().filter((key) => key.startsWith(RootKeyStorageKeyPrefix))
|
||||
|
||||
const keyRawPayloads = storageKeys.map((key) =>
|
||||
this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(key),
|
||||
)
|
||||
|
||||
const keyPayloads = keyRawPayloads.map((rawPayload) => new DecryptedPayload<KeySystemRootKeyContent>(rawPayload))
|
||||
|
||||
const keys = keyPayloads.map((payload) => new KeySystemRootKey(payload))
|
||||
keys.forEach((key) => {
|
||||
this.rootKeyMemoryCache[key.systemIdentifier] = key
|
||||
})
|
||||
}
|
||||
|
||||
private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
|
||||
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
||||
}
|
||||
|
||||
public intakeNonPersistentKeySystemRootKey(
|
||||
key: KeySystemRootKeyInterface,
|
||||
storage: KeySystemRootKeyStorageMode,
|
||||
): void {
|
||||
this.rootKeyMemoryCache[key.systemIdentifier] = key
|
||||
|
||||
if (storage === KeySystemRootKeyStorageMode.Local) {
|
||||
this.storage.setValue(this.storageKeyForRootKey(key.systemIdentifier), key.payload.ejected())
|
||||
}
|
||||
}
|
||||
|
||||
public undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void {
|
||||
delete this.rootKeyMemoryCache[systemIdentifier]
|
||||
void this.storage.removeValue(this.storageKeyForRootKey(systemIdentifier))
|
||||
}
|
||||
|
||||
public getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[] {
|
||||
return this.items.getItems(ContentType.KeySystemRootKey)
|
||||
}
|
||||
|
||||
public clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void {
|
||||
delete this.rootKeyMemoryCache[vault.systemIdentifier]
|
||||
|
||||
const itemsKeys = this.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||
this.items.removeItemsLocally(itemsKeys)
|
||||
}
|
||||
|
||||
public getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] {
|
||||
return this.items.itemsMatchingPredicate<KeySystemRootKeyInterface>(
|
||||
ContentType.KeySystemRootKey,
|
||||
new Predicate<KeySystemRootKeyInterface>('systemIdentifier', '=', systemIdentifier),
|
||||
)
|
||||
}
|
||||
|
||||
public getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] {
|
||||
const synced = this.getSyncedKeySystemRootKeysForVault(systemIdentifier)
|
||||
const memory = this.rootKeyMemoryCache[systemIdentifier] ? [this.rootKeyMemoryCache[systemIdentifier]] : []
|
||||
return [...synced, ...memory]
|
||||
}
|
||||
|
||||
public async deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise<void> {
|
||||
delete this.rootKeyMemoryCache[systemIdentifier]
|
||||
|
||||
await this.storage.removeValue(this.storageKeyForRootKey(systemIdentifier))
|
||||
}
|
||||
|
||||
public async deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise<void> {
|
||||
const keys = this.getSyncedKeySystemRootKeysForVault(systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(keys)
|
||||
}
|
||||
|
||||
public getKeySystemRootKeyWithToken(
|
||||
systemIdentifier: KeySystemIdentifier,
|
||||
rootKeyToken: string,
|
||||
): KeySystemRootKeyInterface | undefined {
|
||||
const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier).filter((key) => key.token === rootKeyToken)
|
||||
|
||||
if (keys.length > 1) {
|
||||
throw new Error('Multiple synced key system root keys found for token')
|
||||
}
|
||||
|
||||
return keys[0]
|
||||
}
|
||||
|
||||
public getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined {
|
||||
const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier)
|
||||
|
||||
const sortedByNewestFirst = keys.sort((a, b) => b.keyParams.creationTimestamp - a.keyParams.creationTimestamp)
|
||||
return sortedByNewestFirst[0]
|
||||
}
|
||||
|
||||
public getAllKeySystemItemsKeys(): (KeySystemItemsKeyInterface | EncryptedItemInterface)[] {
|
||||
const decryptedItems = this.items.getItems<KeySystemItemsKeyInterface>(ContentType.KeySystemItemsKey)
|
||||
const encryptedItems = this.items.invalidItems.filter((item) => item.content_type === ContentType.KeySystemItemsKey)
|
||||
return [...decryptedItems, ...encryptedItems]
|
||||
}
|
||||
|
||||
public getKeySystemItemsKeys(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface[] {
|
||||
return this.items
|
||||
.getItems<KeySystemItemsKeyInterface>(ContentType.KeySystemItemsKey)
|
||||
.filter((key) => key.key_system_identifier === systemIdentifier)
|
||||
}
|
||||
|
||||
public getPrimaryKeySystemItemsKey(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface {
|
||||
const rootKey = this.getPrimaryKeySystemRootKey(systemIdentifier)
|
||||
if (!rootKey) {
|
||||
throw new Error('No primary key system root key found')
|
||||
}
|
||||
|
||||
const matchingItemsKeys = this.getKeySystemItemsKeys(systemIdentifier).filter(
|
||||
(key) => key.rootKeyToken === rootKey.token,
|
||||
)
|
||||
|
||||
const sortedByNewestFirst = matchingItemsKeys.sort((a, b) => b.creationTimestamp - a.creationTimestamp)
|
||||
return sortedByNewestFirst[0]
|
||||
}
|
||||
}
|
||||
146
packages/services/src/Domain/Mutator/ImportDataUseCase.ts
Normal file
146
packages/services/src/Domain/Mutator/ImportDataUseCase.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||
import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
|
||||
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ComponentContent,
|
||||
CopyPayloadWithContentOverride,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedItemInterface,
|
||||
DecryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||
|
||||
const Strings = {
|
||||
UnsupportedBackupFileVersion:
|
||||
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
|
||||
BackupFileMoreRecentThanAccount:
|
||||
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
|
||||
FileAccountPassword: 'File account password',
|
||||
}
|
||||
|
||||
export type ImportDataReturnType =
|
||||
| {
|
||||
affectedItems: DecryptedItemInterface[]
|
||||
errorCount: number
|
||||
}
|
||||
| {
|
||||
error: ClientDisplayableError
|
||||
}
|
||||
|
||||
export class ImportDataUseCase {
|
||||
constructor(
|
||||
private itemManager: ItemManagerInterface,
|
||||
private syncService: SyncServiceInterface,
|
||||
private protectionService: ProtectionsClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
private challengeService: ChallengeServiceInterface,
|
||||
private historyService: HistoryServiceInterface,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* .affectedItems: Items that were either created or dirtied by this import
|
||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||
*/
|
||||
|
||||
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||
if (data.version) {
|
||||
/**
|
||||
* Prior to 003 backup files did not have a version field so we cannot
|
||||
* stop importing if there is no backup file version, only if there is
|
||||
* an unsupported version.
|
||||
*/
|
||||
const version = data.version as ProtocolVersion
|
||||
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return { error: new ClientDisplayableError(Strings.UnsupportedBackupFileVersion) }
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) }
|
||||
}
|
||||
}
|
||||
|
||||
let password: string | undefined
|
||||
|
||||
if (data.auth_params || data.keyParams) {
|
||||
/** Get import file password. */
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
||||
ChallengeReason.DecryptEncryptedFile,
|
||||
true,
|
||||
)
|
||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (passwordResponse == undefined) {
|
||||
/** Challenge was canceled */
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
password = passwordResponse?.values[0].value as string
|
||||
}
|
||||
|
||||
if (!(await this.protectionService.authorizeFileImport())) {
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
}
|
||||
|
||||
data.items = data.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return CreateEncryptedBackupFileContextPayload(item)
|
||||
} else {
|
||||
return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload)
|
||||
}
|
||||
})
|
||||
|
||||
const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
}
|
||||
|
||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
||||
/* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess */
|
||||
if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) {
|
||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
||||
return CopyPayloadWithContentOverride(typedContent, {
|
||||
active: false,
|
||||
})
|
||||
} else {
|
||||
return payload
|
||||
}
|
||||
})
|
||||
|
||||
const affectedUuids = await this.payloadManager.importPayloads(
|
||||
validPayloads,
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
)
|
||||
|
||||
const promise = this.syncService.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
}
|
||||
|
||||
const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[]
|
||||
|
||||
return {
|
||||
affectedItems: affectedItems,
|
||||
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,92 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
ComponentMutator,
|
||||
DecryptedItemInterface,
|
||||
DecryptedItemMutator,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedItemInterface,
|
||||
FeatureRepoMutator,
|
||||
FileItem,
|
||||
ItemContent,
|
||||
ItemsKeyInterface,
|
||||
ItemsKeyMutatorInterface,
|
||||
MutationType,
|
||||
PayloadEmitSource,
|
||||
PredicateInterface,
|
||||
SmartView,
|
||||
SNComponent,
|
||||
SNFeatureRepo,
|
||||
SNNote,
|
||||
SNTag,
|
||||
TransactionalMutation,
|
||||
VaultListingInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
|
||||
import { ChallengeReason } from '../Challenge/Types/ChallengeReason'
|
||||
import { SyncOptions } from '../Sync/SyncOptions'
|
||||
|
||||
export interface MutatorClientInterface {
|
||||
/**
|
||||
* Inserts the input item by its payload properties, and marks the item as dirty.
|
||||
* A sync is not performed after an item is inserted. This must be handled by the caller.
|
||||
*/
|
||||
insertItem(item: DecryptedItemInterface): Promise<DecryptedItemInterface>
|
||||
insertItem<T extends DecryptedItemInterface>(item: DecryptedItemInterface, setDirty?: boolean): Promise<T>
|
||||
emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing item, marks it as dirty, and syncs it
|
||||
*/
|
||||
changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
|
||||
/**
|
||||
* Mutates pre-existing items, marks them as dirty, and syncs
|
||||
*/
|
||||
changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise<void>
|
||||
setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise<void>
|
||||
setItemsDirty(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
isUserModified?: boolean,
|
||||
): Promise<DecryptedItemInterface[]>
|
||||
createItem<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
|
||||
contentType: ContentType,
|
||||
content: C,
|
||||
needsSync?: boolean,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<T>
|
||||
|
||||
changeItem<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemToLookupUuidFor: I,
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<void>
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I>
|
||||
changeItems<
|
||||
M extends DecryptedItemMutator = DecryptedItemMutator,
|
||||
I extends DecryptedItemInterface = DecryptedItemInterface,
|
||||
>(
|
||||
itemsToLookupUuidsFor: I[],
|
||||
mutate?: (mutator: M) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<I[]>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing item and marks it as dirty. Does not sync changes.
|
||||
*/
|
||||
changeItem<M extends DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
changeItemsKey(
|
||||
itemToLookupUuidFor: ItemsKeyInterface,
|
||||
mutate: (mutator: ItemsKeyMutatorInterface) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<ItemsKeyInterface>
|
||||
|
||||
/**
|
||||
* Mutates a pre-existing items and marks them as dirty. Does not sync changes.
|
||||
*/
|
||||
changeItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps?: boolean,
|
||||
): Promise<(DecryptedItemInterface | undefined)[]>
|
||||
changeComponent(
|
||||
itemToLookupUuidFor: SNComponent,
|
||||
mutate: (mutator: ComponentMutator) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNComponent>
|
||||
|
||||
changeFeatureRepo(
|
||||
itemToLookupUuidFor: SNFeatureRepo,
|
||||
mutate: (mutator: FeatureRepoMutator) => void,
|
||||
mutationType?: MutationType,
|
||||
emitSource?: PayloadEmitSource,
|
||||
payloadSourceKey?: string,
|
||||
): Promise<SNFeatureRepo>
|
||||
|
||||
/**
|
||||
* Run unique mutations per each item in the array, then only propagate all changes
|
||||
@@ -83,44 +105,11 @@ export interface MutatorClientInterface {
|
||||
payloadSourceKey?: string,
|
||||
): Promise<DecryptedItemInterface | undefined>
|
||||
|
||||
protectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
|
||||
items: I[],
|
||||
): Promise<I[]>
|
||||
|
||||
unprotectItems<_M extends DecryptedItemMutator<ItemContent>, I extends DecryptedItemInterface<ItemContent>>(
|
||||
items: I[],
|
||||
reason: ChallengeReason,
|
||||
): Promise<I[] | undefined>
|
||||
|
||||
protectNote(note: SNNote): Promise<SNNote>
|
||||
|
||||
unprotectNote(note: SNNote): Promise<SNNote | undefined>
|
||||
|
||||
protectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
|
||||
unprotectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
|
||||
protectFile(file: FileItem): Promise<FileItem>
|
||||
|
||||
unprotectFile(file: FileItem): Promise<FileItem | undefined>
|
||||
|
||||
/**
|
||||
* Takes the values of the input item and emits it onto global state.
|
||||
*/
|
||||
mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
|
||||
|
||||
/**
|
||||
* Creates an unmanaged item that can be added later.
|
||||
*/
|
||||
createTemplateItem<
|
||||
C extends ItemContent = ItemContent,
|
||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
contentType: ContentType,
|
||||
content?: C,
|
||||
override?: Partial<DecryptedPayload<C>>,
|
||||
): I
|
||||
|
||||
/**
|
||||
* @param isUserModified Whether to change the modified date the user
|
||||
* sees of the item.
|
||||
@@ -135,7 +124,13 @@ export interface MutatorClientInterface {
|
||||
|
||||
emptyTrash(): Promise<void>
|
||||
|
||||
duplicateItem<T extends DecryptedItemInterface>(item: T, additionalContent?: Partial<T['content']>): Promise<T>
|
||||
duplicateItem<T extends DecryptedItemInterface>(
|
||||
itemToLookupUuidFor: T,
|
||||
isConflict?: boolean,
|
||||
additionalContent?: Partial<T['content']>,
|
||||
): Promise<T>
|
||||
|
||||
addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
|
||||
|
||||
/**
|
||||
* Migrates any tags containing a '.' character to sa chema-based heirarchy, removing
|
||||
@@ -146,41 +141,35 @@ export interface MutatorClientInterface {
|
||||
/**
|
||||
* Establishes a hierarchical relationship between two tags.
|
||||
*/
|
||||
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<void>
|
||||
setTagParent(parentTag: SNTag, childTag: SNTag): Promise<SNTag>
|
||||
|
||||
/**
|
||||
* Remove the tag parent.
|
||||
*/
|
||||
unsetTagParent(childTag: SNTag): Promise<void>
|
||||
unsetTagParent(childTag: SNTag): Promise<SNTag>
|
||||
|
||||
findOrCreateTag(title: string): Promise<SNTag>
|
||||
findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise<SNTag>
|
||||
|
||||
/** Creates and returns the tag but does not run sync. Callers must perform sync. */
|
||||
createTagOrSmartView(title: string): Promise<SNTag | SmartView>
|
||||
createTagOrSmartView<T extends SNTag | SmartView>(title: string, vault?: VaultListingInterface): Promise<T>
|
||||
findOrCreateTagParentChain(titlesHierarchy: string[]): Promise<SNTag>
|
||||
|
||||
/**
|
||||
* Activates or deactivates a component, depending on its
|
||||
* current state, and syncs.
|
||||
*/
|
||||
toggleComponent(component: SNComponent): Promise<void>
|
||||
associateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem | undefined>
|
||||
|
||||
toggleTheme(theme: SNComponent): Promise<void>
|
||||
disassociateFileWithNote(file: FileItem, note: SNNote): Promise<FileItem>
|
||||
renameFile(file: FileItem, name: string): Promise<FileItem>
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* .affectedItems: Items that were either created or dirtied by this import
|
||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||
*/
|
||||
importData(
|
||||
data: BackupFile,
|
||||
awaitSync?: boolean,
|
||||
): Promise<
|
||||
| {
|
||||
affectedItems: DecryptedItemInterface[]
|
||||
errorCount: number
|
||||
}
|
||||
| {
|
||||
error: ClientDisplayableError
|
||||
}
|
||||
>
|
||||
unlinkItems(
|
||||
itemA: DecryptedItemInterface<ItemContent>,
|
||||
itemB: DecryptedItemInterface<ItemContent>,
|
||||
): Promise<DecryptedItemInterface<ItemContent>>
|
||||
createSmartView<T extends DecryptedItemInterface>(dto: {
|
||||
title: string
|
||||
predicate: PredicateInterface<T>
|
||||
iconString?: string
|
||||
vault?: VaultListingInterface
|
||||
}): Promise<SmartView>
|
||||
linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote>
|
||||
linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem>
|
||||
addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise<SNTag[] | undefined>
|
||||
}
|
||||
|
||||
@@ -25,4 +25,6 @@ export interface PayloadManagerInterface {
|
||||
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
||||
|
||||
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||
|
||||
removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DecryptedItem } from '@standardnotes/models'
|
||||
import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models'
|
||||
import { ChallengeReason } from '../Challenge'
|
||||
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
||||
import { TimingDisplayOption } from './TimingDisplayOption'
|
||||
@@ -24,4 +24,13 @@ export interface ProtectionsClientInterface {
|
||||
authorizeAddingPasscode(): Promise<boolean>
|
||||
authorizeRemovingPasscode(): Promise<boolean>
|
||||
authorizeChangingPasscode(): Promise<boolean>
|
||||
authorizeFileImport(): 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>
|
||||
unprotectNote(note: SNNote): Promise<SNNote | undefined>
|
||||
protectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
unprotectNotes(notes: SNNote[]): Promise<SNNote[]>
|
||||
protectFile(file: FileItem): Promise<FileItem>
|
||||
unprotectFile(file: FileItem): Promise<FileItem | undefined>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { RevisionPayload } from './RevisionPayload'
|
||||
|
||||
export interface RevisionClientInterface {
|
||||
listRevisions(itemUuid: Uuid): Promise<
|
||||
@@ -11,18 +12,5 @@ export interface RevisionClientInterface {
|
||||
}>
|
||||
>
|
||||
deleteRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise<string>
|
||||
getRevision(
|
||||
itemUuid: Uuid,
|
||||
revisionUuid: Uuid,
|
||||
): Promise<{
|
||||
uuid: string
|
||||
item_uuid: string
|
||||
content: string | null
|
||||
content_type: string
|
||||
items_key_id: string | null
|
||||
enc_item_key: string | null
|
||||
auth_hash: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
} | null>
|
||||
getRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise<RevisionPayload | null>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isErrorResponse } from '@standardnotes/responses'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { RevisionClientInterface } from './RevisionClientInterface'
|
||||
import { RevisionPayload } from './RevisionPayload'
|
||||
|
||||
export class RevisionManager extends AbstractService implements RevisionClientInterface {
|
||||
constructor(
|
||||
@@ -36,20 +37,7 @@ export class RevisionManager extends AbstractService implements RevisionClientIn
|
||||
return result.data.message
|
||||
}
|
||||
|
||||
async getRevision(
|
||||
itemUuid: Uuid,
|
||||
revisionUuid: Uuid,
|
||||
): Promise<{
|
||||
uuid: string
|
||||
item_uuid: string
|
||||
content: string | null
|
||||
content_type: string
|
||||
items_key_id: string | null
|
||||
enc_item_key: string | null
|
||||
auth_hash: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
} | null> {
|
||||
async getRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise<RevisionPayload | null> {
|
||||
const result = await this.revisionApiService.getRevision(itemUuid.value, revisionUuid.value)
|
||||
|
||||
if (isErrorResponse(result)) {
|
||||
|
||||
14
packages/services/src/Domain/Revision/RevisionPayload.ts
Normal file
14
packages/services/src/Domain/Revision/RevisionPayload.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type RevisionPayload = {
|
||||
uuid: string
|
||||
item_uuid: string
|
||||
content: string | null
|
||||
content_type: string
|
||||
items_key_id: string | null
|
||||
enc_item_key: string | null
|
||||
auth_hash: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
user_uuid: string
|
||||
key_system_identifier: string | null
|
||||
shared_vault_uuid: string | null
|
||||
}
|
||||
@@ -8,13 +8,15 @@ import { ApplicationStage } from '../Application/ApplicationStage'
|
||||
import { InternalEventPublishStrategy } from '../Internal/InternalEventPublishStrategy'
|
||||
import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics'
|
||||
|
||||
export abstract class AbstractService<EventName = string, EventData = undefined>
|
||||
export abstract class AbstractService<EventName = string, EventData = unknown>
|
||||
implements ServiceInterface<EventName, EventData>
|
||||
{
|
||||
private eventObservers: EventObserver<EventName, EventData>[] = []
|
||||
public loggingEnabled = false
|
||||
private criticalPromises: Promise<unknown>[] = []
|
||||
|
||||
protected eventDisposers: (() => void)[] = []
|
||||
|
||||
constructor(protected internalEventBus: InternalEventBusInterface) {}
|
||||
|
||||
public addEventObserver(observer: EventObserver<EventName, EventData>): () => void {
|
||||
@@ -71,6 +73,11 @@ export abstract class AbstractService<EventName = string, EventData = undefined>
|
||||
this.eventObservers.length = 0
|
||||
;(this.internalEventBus as unknown) = undefined
|
||||
;(this.criticalPromises as unknown) = undefined
|
||||
|
||||
for (const disposer of this.eventDisposers) {
|
||||
disposer()
|
||||
}
|
||||
this.eventDisposers = []
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
packages/services/src/Domain/Session/SessionEvent.ts
Normal file
5
packages/services/src/Domain/Session/SessionEvent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum SessionEvent {
|
||||
Restored = 'SessionRestored',
|
||||
Revoked = 'SessionRevoked',
|
||||
UserKeyPairChanged = 'UserKeyPairChanged',
|
||||
}
|
||||
@@ -10,7 +10,11 @@ import { SessionManagerResponse } from './SessionManagerResponse'
|
||||
export interface SessionsClientInterface {
|
||||
getWorkspaceDisplayIdentifier(): string
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
|
||||
getUser(): User | undefined
|
||||
get userUuid(): string
|
||||
getSureUser(): User
|
||||
|
||||
isCurrentSessionReadOnly(): boolean | undefined
|
||||
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>
|
||||
signIn(
|
||||
@@ -20,7 +24,7 @@ export interface SessionsClientInterface {
|
||||
ephemeral: boolean,
|
||||
minAllowedVersion?: ProtocolVersion,
|
||||
): Promise<SessionManagerResponse>
|
||||
getSureUser(): User
|
||||
isSignedIn(): boolean
|
||||
bypassChecksAndSignInWithRootKey(
|
||||
email: string,
|
||||
rootKey: RootKeyInterface,
|
||||
@@ -42,4 +46,8 @@ export interface SessionsClientInterface {
|
||||
rootKey: SNRootKey
|
||||
wrappingKey?: SNRootKey
|
||||
}): Promise<void>
|
||||
|
||||
getPublicKey(): string
|
||||
getSigningPublicKey(): string
|
||||
isUserMissingKeyPair(): boolean
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export type UserKeyPairChangedEventData = {
|
||||
oldKeyPair: PkcKeyPair | undefined
|
||||
oldSigningKeyPair: PkcKeyPair | undefined
|
||||
|
||||
newKeyPair: PkcKeyPair
|
||||
newSigningKeyPair: PkcKeyPair
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
||||
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
|
||||
|
||||
export type PendingSharedVaultInviteRecord = {
|
||||
invite: SharedVaultInviteServerHash
|
||||
message: AsymmetricMessageSharedVaultInvite
|
||||
trusted: boolean
|
||||
}
|
||||
587
packages/services/src/Domain/SharedVaults/SharedVaultService.ts
Normal file
587
packages/services/src/Domain/SharedVaults/SharedVaultService.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
|
||||
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||
import { InviteContactToSharedVaultUseCase } from './UseCase/InviteContactToSharedVault'
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
isErrorResponse,
|
||||
SharedVaultUserServerHash,
|
||||
isClientDisplayableError,
|
||||
SharedVaultPermission,
|
||||
UserEventType,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
HttpServiceInterface,
|
||||
SharedVaultServerInterface,
|
||||
SharedVaultUsersServerInterface,
|
||||
SharedVaultInvitesServerInterface,
|
||||
SharedVaultUsersServer,
|
||||
SharedVaultInvitesServer,
|
||||
SharedVaultServer,
|
||||
AsymmetricMessageServerInterface,
|
||||
AsymmetricMessageServer,
|
||||
} from '@standardnotes/api'
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
PayloadEmitSource,
|
||||
TrustedContactInterface,
|
||||
SharedVaultListingInterface,
|
||||
VaultListingInterface,
|
||||
AsymmetricMessageSharedVaultInvite,
|
||||
KeySystemRootKeyStorageMode,
|
||||
} from '@standardnotes/models'
|
||||
import { SharedVaultServiceInterface } from './SharedVaultServiceInterface'
|
||||
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { GetSharedVaultUsersUseCase } from './UseCase/GetSharedVaultUsers'
|
||||
import { RemoveVaultMemberUseCase } 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 { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
|
||||
import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent'
|
||||
import { DeleteExternalSharedVaultUseCase } from './UseCase/DeleteExternalSharedVault'
|
||||
import { DeleteSharedVaultUseCase } from './UseCase/DeleteSharedVault'
|
||||
import { VaultServiceEvent, VaultServiceEventPayload } from '../Vaults/VaultServiceEvent'
|
||||
import { AcceptTrustedSharedVaultInvite } from './UseCase/AcceptTrustedSharedVaultInvite'
|
||||
import { GetAsymmetricMessageTrustedPayload } from '../AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload'
|
||||
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'
|
||||
|
||||
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,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
|
||||
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
|
||||
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.TrustedContact, ({ changed, inserted, source }) => {
|
||||
if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
|
||||
void this.handleCreationOfNewTrustedContacts(inserted)
|
||||
}
|
||||
if (source === PayloadEmitSource.LocalChanged && changed.length > 0) {
|
||||
void this.handleTrustedContactsChange(changed)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
this.eventDisposers.push(
|
||||
items.addObserver<VaultListingInterface>(ContentType.VaultListing, ({ changed, source }) => {
|
||||
if (source === PayloadEmitSource.LocalChanged && changed.length > 0) {
|
||||
void this.handleVaultListingsChange(changed)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SessionEvent.UserKeyPairChanged) {
|
||||
void this.invitesServer.deleteAllInboundInvites()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} else if (event.eventPayload.eventType === UserEventType.SharedVaultItemRemoved) {
|
||||
const item = this.items.findItem(event.eventPayload.itemUuid)
|
||||
if (item) {
|
||||
this.items.removeItemsLocally([item])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleVaultRootKeyRotatedEvent(vault: VaultListingInterface): Promise<void> {
|
||||
if (!vault.isSharedVaultListing()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isCurrentUserSharedVaultOwner(vault)) {
|
||||
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 })
|
||||
}
|
||||
|
||||
async createSharedVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
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({
|
||||
vaultName: dto.name,
|
||||
vaultDescription: dto.description,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
storagePreference: dto.storagePreference ?? KeySystemRootKeyStorageMode.Synced,
|
||||
})
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] {
|
||||
return Object.values(this.pendingInvites)
|
||||
}
|
||||
|
||||
private getAllSharedVaults(): SharedVaultListingInterface[] {
|
||||
const vaults = this.vaults.getVaults().filter((vault) => vault.isSharedVaultListing())
|
||||
return vaults as SharedVaultListingInterface[]
|
||||
}
|
||||
|
||||
private findSharedVault(sharedVaultUuid: string): SharedVaultListingInterface | undefined {
|
||||
return this.getAllSharedVaults().find((vault) => vault.sharing.sharedVaultUuid === sharedVaultUuid)
|
||||
}
|
||||
|
||||
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
|
||||
if (!sharedVault.sharing.ownerUserUuid) {
|
||||
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
|
||||
}
|
||||
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
|
||||
}
|
||||
|
||||
public isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean {
|
||||
if (!sharedVault.sharing.ownerUserUuid) {
|
||||
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
|
||||
}
|
||||
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
|
||||
}
|
||||
|
||||
public isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean {
|
||||
const vault = this.findSharedVault(user.shared_vault_uuid)
|
||||
return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid
|
||||
}
|
||||
|
||||
private async handleCreationOfNewTrustedContacts(_contacts: TrustedContactInterface[]): Promise<void> {
|
||||
await this.downloadInboundInvites()
|
||||
}
|
||||
|
||||
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
|
||||
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()
|
||||
|
||||
for (const contact of contacts) {
|
||||
await this.shareContactWithUserAdministeredSharedVaults(contact)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleVaultListingsChange(vaults: VaultListingInterface[]): Promise<void> {
|
||||
for (const vault of vaults) {
|
||||
if (!vault.isSharedVaultListing()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const usecase = new SendSharedVaultMetadataChangedMessageToAll(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
this.usersServer,
|
||||
this.messageServer,
|
||||
)
|
||||
|
||||
await usecase.execute({
|
||||
vault,
|
||||
senderUuid: this.session.getSureUser().uuid,
|
||||
senderEncryptionKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]> {
|
||||
const response = await this.invitesServer.getInboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`)
|
||||
}
|
||||
|
||||
this.pendingInvites = {}
|
||||
|
||||
await this.processInboundInvites(response.data.invites)
|
||||
|
||||
return response.data.invites
|
||||
}
|
||||
|
||||
public async getOutboundInvites(
|
||||
sharedVault?: SharedVaultListingInterface,
|
||||
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError> {
|
||||
const response = await this.invitesServer.getOutboundUserInvites()
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`)
|
||||
}
|
||||
|
||||
if (sharedVault) {
|
||||
return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid)
|
||||
}
|
||||
|
||||
return response.data.invites
|
||||
}
|
||||
|
||||
public async deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void> {
|
||||
const response = await this.invitesServer.deleteInvite({
|
||||
sharedVaultUuid: invite.shared_vault_uuid,
|
||||
inviteUuid: invite.uuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromString(`Failed to delete invite ${response}`)
|
||||
}
|
||||
|
||||
delete this.pendingInvites[invite.uuid]
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
|
||||
const cachedInvites = this.getCachedPendingInviteRecords()
|
||||
|
||||
for (const record of cachedInvites) {
|
||||
if (record.trusted) {
|
||||
continue
|
||||
}
|
||||
|
||||
const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
)
|
||||
|
||||
const trustedMessage = trustedMessageUseCase.execute({
|
||||
message: record.invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
})
|
||||
|
||||
if (trustedMessage) {
|
||||
record.message = trustedMessage
|
||||
record.trusted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
|
||||
if (invites.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const invite of invites) {
|
||||
const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
||||
this.encryption,
|
||||
this.contacts,
|
||||
)
|
||||
|
||||
const trustedMessage = trustedMessageUseCase.execute({
|
||||
message: invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
})
|
||||
|
||||
if (trustedMessage) {
|
||||
this.pendingInvites[invite.uuid] = {
|
||||
invite,
|
||||
message: trustedMessage,
|
||||
trusted: true,
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const untrustedMessageUseCase = new GetAsymmetricMessageUntrustedPayload<AsymmetricMessageSharedVaultInvite>(
|
||||
this.encryption,
|
||||
)
|
||||
|
||||
const untrustedMessage = untrustedMessageUseCase.execute({
|
||||
message: invite,
|
||||
privateKey: this.encryption.getKeyPair().privateKey,
|
||||
})
|
||||
|
||||
if (untrustedMessage) {
|
||||
this.pendingInvites[invite.uuid] = {
|
||||
invite,
|
||||
message: untrustedMessage,
|
||||
trusted: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.notifyCollaborationStatusChanged()
|
||||
}
|
||||
|
||||
private async notifyCollaborationStatusChanged(): Promise<void> {
|
||||
await this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged)
|
||||
}
|
||||
|
||||
async acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void> {
|
||||
if (!pendingInvite.trusted) {
|
||||
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 })
|
||||
|
||||
delete this.pendingInvites[pendingInvite.invite.uuid]
|
||||
|
||||
void this.sync.sync()
|
||||
|
||||
await this.decryptErroredItemsAfterInviteAccept()
|
||||
|
||||
await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid])
|
||||
}
|
||||
|
||||
private async decryptErroredItemsAfterInviteAccept(): Promise<void> {
|
||||
await this.encryption.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
public async getInvitableContactsForSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
): Promise<TrustedContactInterface[]> {
|
||||
const users = await this.getSharedVaultUsers(sharedVault)
|
||||
if (!users) {
|
||||
return []
|
||||
}
|
||||
|
||||
const contacts = this.contacts.getAllContacts()
|
||||
return contacts.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) {
|
||||
return []
|
||||
}
|
||||
|
||||
return contacts
|
||||
}
|
||||
|
||||
async inviteContactToSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
contact: TrustedContactInterface,
|
||||
permissions: SharedVaultPermission,
|
||||
): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
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(),
|
||||
sharedVault,
|
||||
recipient: contact,
|
||||
sharedVaultContacts,
|
||||
permissions,
|
||||
})
|
||||
|
||||
void this.notifyCollaborationStatusChanged()
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async removeUserFromSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
userUuid: string,
|
||||
): Promise<ClientDisplayableError | void> {
|
||||
if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) {
|
||||
throw new Error('Only vault admins can remove users')
|
||||
}
|
||||
|
||||
if (this.vaults.isVaultLocked(sharedVault)) {
|
||||
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 })
|
||||
if (isClientDisplayableError(result)) {
|
||||
return result
|
||||
}
|
||||
|
||||
void this.notifyCollaborationStatusChanged()
|
||||
|
||||
await this.vaults.rotateVaultRootKey(sharedVault)
|
||||
}
|
||||
|
||||
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({
|
||||
sharedVault: sharedVault,
|
||||
userUuid: this.session.getSureUser().uuid,
|
||||
})
|
||||
|
||||
if (isClientDisplayableError(result)) {
|
||||
return result
|
||||
}
|
||||
|
||||
void this.notifyCollaborationStatusChanged()
|
||||
}
|
||||
|
||||
async getSharedVaultUsers(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
): Promise<SharedVaultUserServerHash[] | undefined> {
|
||||
const useCase = new GetSharedVaultUsersUseCase(this.usersServer)
|
||||
return useCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
|
||||
}
|
||||
|
||||
private async shareContactWithUserAdministeredSharedVaults(contact: TrustedContactInterface): Promise<void> {
|
||||
const sharedVaults = this.getAllSharedVaults()
|
||||
|
||||
const useCase = new ShareContactWithAllMembersOfSharedVaultUseCase(
|
||||
this.contacts,
|
||||
this.encryption,
|
||||
this.usersServer,
|
||||
this.messageServer,
|
||||
)
|
||||
|
||||
for (const vault of sharedVaults) {
|
||||
if (!this.isCurrentUserSharedVaultAdmin(vault)) {
|
||||
continue
|
||||
}
|
||||
|
||||
await useCase.execute({
|
||||
senderKeyPair: this.encryption.getKeyPair(),
|
||||
senderSigningKeyPair: this.encryption.getSigningKeyPair(),
|
||||
sharedVault: vault,
|
||||
contactToShare: contact,
|
||||
senderUserUuid: this.session.getSureUser().uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined {
|
||||
if (!item.last_edited_by_uuid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contact = this.contacts.findTrustedContact(item.last_edited_by_uuid)
|
||||
|
||||
return contact
|
||||
}
|
||||
|
||||
getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined {
|
||||
if (!item.user_uuid || item.user_uuid === this.session.getSureUser().uuid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contact = this.contacts.findTrustedContact(item.user_uuid)
|
||||
|
||||
return contact
|
||||
}
|
||||
|
||||
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.vaults as unknown) = undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { KeySystemIdentifier } from '@standardnotes/models'
|
||||
|
||||
export enum SharedVaultServiceEvent {
|
||||
SharedVaultStatusChanged = 'SharedVaultStatusChanged',
|
||||
}
|
||||
|
||||
export type SharedVaultServiceEventPayload = {
|
||||
sharedVaultUuid: string
|
||||
keySystemIdentifier: KeySystemIdentifier
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
SharedVaultUserServerHash,
|
||||
SharedVaultPermission,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
DecryptedItemInterface,
|
||||
TrustedContactInterface,
|
||||
SharedVaultListingInterface,
|
||||
VaultListingInterface,
|
||||
KeySystemRootKeyStorageMode,
|
||||
} from '@standardnotes/models'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
|
||||
import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord'
|
||||
|
||||
export interface SharedVaultServiceInterface
|
||||
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload> {
|
||||
createSharedVault(dto: {
|
||||
name: string
|
||||
description?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference?: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface | ClientDisplayableError>
|
||||
deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
|
||||
|
||||
convertVaultToSharedVault(vault: VaultListingInterface): Promise<SharedVaultListingInterface | ClientDisplayableError>
|
||||
|
||||
inviteContactToSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
contact: TrustedContactInterface,
|
||||
permissions: SharedVaultPermission,
|
||||
): Promise<SharedVaultInviteServerHash | ClientDisplayableError>
|
||||
removeUserFromSharedVault(
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
userUuid: string,
|
||||
): Promise<ClientDisplayableError | void>
|
||||
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
|
||||
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
|
||||
isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean
|
||||
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
|
||||
|
||||
getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
|
||||
getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
|
||||
|
||||
downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]>
|
||||
getOutboundInvites(
|
||||
sharedVault?: SharedVaultListingInterface,
|
||||
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError>
|
||||
acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void>
|
||||
getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[]
|
||||
getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]>
|
||||
deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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,47 @@
|
||||
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 { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class ConvertToSharedVaultUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: { vault: VaultListingInterface }): Promise<SharedVaultListingInterface | ClientDisplayableError> {
|
||||
if (dto.vault.isSharedVaultListing()) {
|
||||
throw new Error('Cannot convert a shared vault to a shared vault')
|
||||
}
|
||||
|
||||
const serverResult = await this.sharedVaultServer.createSharedVault()
|
||||
if (isErrorResponse(serverResult)) {
|
||||
return ClientDisplayableError.FromString(`Failed to create shared vault ${serverResult}`)
|
||||
}
|
||||
|
||||
const serverVaultHash = serverResult.data.sharedVault
|
||||
|
||||
const sharedVaultListing = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(
|
||||
dto.vault,
|
||||
(mutator) => {
|
||||
mutator.sharing = {
|
||||
sharedVaultUuid: serverVaultHash.uuid,
|
||||
ownerUserUuid: serverVaultHash.user_uuid,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier)
|
||||
const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files)
|
||||
await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
return sharedVaultListing as SharedVaultListingInterface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import {
|
||||
KeySystemRootKeyStorageMode,
|
||||
SharedVaultListingInterface,
|
||||
VaultListingInterface,
|
||||
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 { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class CreateSharedVaultUseCase {
|
||||
constructor(
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private files: FilesClientInterface,
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<SharedVaultListingInterface | ClientDisplayableError> {
|
||||
const usecase = new CreateVaultUseCase(this.mutator, this.encryption, this.sync)
|
||||
const privateVault = await usecase.execute({
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
storagePreference: dto.storagePreference,
|
||||
})
|
||||
|
||||
const serverResult = await this.sharedVaultServer.createSharedVault()
|
||||
if (isErrorResponse(serverResult)) {
|
||||
return ClientDisplayableError.FromString(`Failed to create shared vault ${JSON.stringify(serverResult)}`)
|
||||
}
|
||||
|
||||
const serverVaultHash = serverResult.data.sharedVault
|
||||
|
||||
const sharedVaultListing = await this.mutator.changeItem<VaultListingMutator, VaultListingInterface>(
|
||||
privateVault,
|
||||
(mutator) => {
|
||||
mutator.sharing = {
|
||||
sharedVaultUuid: serverVaultHash.uuid,
|
||||
ownerUserUuid: serverVaultHash.user_uuid,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier)
|
||||
const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files)
|
||||
await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems })
|
||||
|
||||
return sharedVaultListing as SharedVaultListingInterface
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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 { Uuids } from '@standardnotes/utils'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class DeleteExternalSharedVaultUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(vault: VaultListingInterface): Promise<void> {
|
||||
await this.deleteDataSharedByVaultUsers(vault)
|
||||
await this.deleteDataOwnedByThisUser(vault)
|
||||
await this.encryption.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier)
|
||||
|
||||
void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' })
|
||||
}
|
||||
|
||||
/**
|
||||
* This data is shared with all vault users and does not belong to this particular user
|
||||
* The data will be removed locally without syncing the items
|
||||
*/
|
||||
private async deleteDataSharedByVaultUsers(vault: VaultListingInterface): Promise<void> {
|
||||
const vaultItems = this.items
|
||||
.allTrackedItems()
|
||||
.filter((item) => item.key_system_identifier === vault.systemIdentifier)
|
||||
this.items.removeItemsLocally(vaultItems as AnyItemInterface[])
|
||||
|
||||
const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier)
|
||||
this.items.removeItemsLocally(itemsKeys)
|
||||
|
||||
await this.storage.deletePayloadsWithUuids([...Uuids(vaultItems), ...Uuids(itemsKeys)])
|
||||
}
|
||||
|
||||
private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise<void> {
|
||||
const rootKeys = this.encryption.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
await this.mutator.setItemsToBeDeleted(rootKeys)
|
||||
|
||||
await this.mutator.setItemToBeDeleted(vault)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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'
|
||||
|
||||
export class DeleteSharedVaultUseCase {
|
||||
constructor(
|
||||
private sharedVaultServer: SharedVaultServerInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: { sharedVault: SharedVaultListingInterface }): Promise<ClientDisplayableError | void> {
|
||||
const response = await this.sharedVaultServer.deleteSharedVault({
|
||||
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
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.sync.sync()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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,16 @@
|
||||
import { SharedVaultUserServerHash, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class GetSharedVaultUsersUseCase {
|
||||
constructor(private vaultUsersServer: SharedVaultUsersServerInterface) {}
|
||||
|
||||
async execute(params: { sharedVaultUuid: string }): Promise<SharedVaultUserServerHash[] | undefined> {
|
||||
const response = await this.vaultUsersServer.getSharedVaultUsers({ sharedVaultUuid: params.sharedVaultUuid })
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return response.data.users
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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,48 @@
|
||||
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 { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import { SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
|
||||
export class LeaveVaultUseCase {
|
||||
constructor(
|
||||
private vaultUserServer: SharedVaultUsersServerInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(params: {
|
||||
sharedVault: SharedVaultListingInterface
|
||||
userUuid: string
|
||||
}): Promise<ClientDisplayableError | void> {
|
||||
const latestVaultListing = this.items.findItem<SharedVaultListingInterface>(params.sharedVault.uuid)
|
||||
if (!latestVaultListing) {
|
||||
throw new Error(`LeaveVaultUseCase: Could not find vault ${params.sharedVault.uuid}`)
|
||||
}
|
||||
|
||||
const response = await this.vaultUserServer.deleteSharedVaultUser({
|
||||
sharedVaultUuid: latestVaultListing.sharing.sharedVaultUuid,
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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,17 @@
|
||||
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class RemoveVaultMemberUseCase {
|
||||
constructor(private vaultUserServer: SharedVaultUsersServerInterface) {}
|
||||
|
||||
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<ClientDisplayableError | void> {
|
||||
const response = await this.vaultUserServer.deleteSharedVaultUser({
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
userUuid: params.userUuid,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromNetworkError(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
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,31 @@
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
isErrorResponse,
|
||||
SharedVaultPermission,
|
||||
} from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class SendSharedVaultInviteUseCase {
|
||||
constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {}
|
||||
|
||||
async execute(params: {
|
||||
sharedVaultUuid: string
|
||||
recipientUuid: string
|
||||
encryptedMessage: string
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
const response = await this.vaultInvitesServer.createInvite({
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
recipientUuid: params.recipientUuid,
|
||||
encryptedMessage: params.encryptedMessage,
|
||||
permissions: params.permissions,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromError(response.data.error)
|
||||
}
|
||||
|
||||
return response.data.invite
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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,78 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
ClientDisplayableError,
|
||||
SharedVaultInviteServerHash,
|
||||
isErrorResponse,
|
||||
SharedVaultPermission,
|
||||
} from '@standardnotes/responses'
|
||||
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
|
||||
|
||||
export class UpdateSharedVaultInviteUseCase {
|
||||
constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {}
|
||||
|
||||
async execute(params: {
|
||||
sharedVaultUuid: string
|
||||
inviteUuid: string
|
||||
encryptedMessage: string
|
||||
permissions: SharedVaultPermission
|
||||
}): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
|
||||
const response = await this.vaultInvitesServer.updateInvite({
|
||||
sharedVaultUuid: params.sharedVaultUuid,
|
||||
inviteUuid: params.inviteUuid,
|
||||
encryptedMessage: params.encryptedMessage,
|
||||
permissions: params.permissions,
|
||||
})
|
||||
|
||||
if (isErrorResponse(response)) {
|
||||
return ClientDisplayableError.FromError(response.data.error)
|
||||
}
|
||||
|
||||
return response.data.invite
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DecryptedItemInterface, ItemContent, Predicate, PredicateInterface } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export interface SingletonManagerInterface {
|
||||
findSingleton<T extends DecryptedItemInterface>(
|
||||
contentType: ContentType,
|
||||
predicate: PredicateInterface<T>,
|
||||
): T | undefined
|
||||
|
||||
findOrCreateContentTypeSingleton<
|
||||
C extends ItemContent = ItemContent,
|
||||
T extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
contentType: ContentType,
|
||||
createContent: ItemContent,
|
||||
): Promise<T>
|
||||
|
||||
findOrCreateSingleton<
|
||||
C extends ItemContent = ItemContent,
|
||||
T extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||
>(
|
||||
predicate: Predicate<T>,
|
||||
contentType: ContentType,
|
||||
createContent: ItemContent,
|
||||
): Promise<T>
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export enum StorageKey {
|
||||
PlaintextBackupsLocation = 'plaintext_backups_location',
|
||||
FileBackupsEnabled = 'file_backups_enabled',
|
||||
FileBackupsLocation = 'file_backups_location',
|
||||
VaultSelectionOptions = 'vault_selection_options',
|
||||
}
|
||||
|
||||
export enum NonwrappedStorageKey {
|
||||
|
||||
@@ -8,14 +8,16 @@ import { StoragePersistencePolicies, StorageValueModes } from './StorageTypes'
|
||||
|
||||
export interface StorageServiceInterface {
|
||||
getAllRawPayloads(): Promise<FullyFormedTransferPayload[]>
|
||||
getAllKeys(mode?: StorageValueModes): string[]
|
||||
getValue<T>(key: string, mode?: StorageValueModes, defaultValue?: T): T
|
||||
canDecryptWithKey(key: RootKeyInterface): Promise<boolean>
|
||||
savePayload(payload: PayloadInterface): Promise<void>
|
||||
savePayloads(decryptedPayloads: PayloadInterface[]): Promise<void>
|
||||
setValue(key: string, value: unknown, mode?: StorageValueModes): void
|
||||
setValue<T>(key: string, value: T, mode?: StorageValueModes): void
|
||||
removeValue(key: string, mode?: StorageValueModes): Promise<void>
|
||||
setPersistencePolicy(persistencePolicy: StoragePersistencePolicies): Promise<void>
|
||||
clearAllData(): Promise<void>
|
||||
forceDeletePayloads(payloads: FullyFormedPayloadInterface[]): Promise<void>
|
||||
deletePayloads(payloads: FullyFormedPayloadInterface[]): Promise<void>
|
||||
deletePayloadsWithUuids(uuids: string[]): Promise<void>
|
||||
clearAllPayloads(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export const InfoStrings = {
|
||||
AccountDeleted: 'Your account has been successfully deleted.',
|
||||
UnsupportedBackupFileVersion:
|
||||
'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.',
|
||||
BackupFileMoreRecentThanAccount:
|
||||
"This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.",
|
||||
|
||||
InvalidNote:
|
||||
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.",
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ export const ChallengeStrings = {
|
||||
DisableMfa: 'Authentication is required to disable two-factor authentication',
|
||||
DeleteAccount: 'Authentication is required to delete your account',
|
||||
ListedAuthorization: 'Authentication is required to approve this note for Listed',
|
||||
UnlockVault: (vaultName: string) => `Unlock ${vaultName}`,
|
||||
EnterVaultPassword: 'Enter the password for this vault',
|
||||
}
|
||||
|
||||
export const ErrorAlertStrings = {
|
||||
|
||||
@@ -19,4 +19,10 @@ export type SyncOptions = {
|
||||
* and before the sync request is network dispatched
|
||||
*/
|
||||
onPresyncSave?: () => void
|
||||
|
||||
/** If supplied, the sync will be exclusive to items in these sharedVaults */
|
||||
sharedVaultUuids?: string[]
|
||||
|
||||
/** If true and sharedVaultUuids is present, excludes sending global syncToken as part of request */
|
||||
syncSharedVaultsFromScratch?: boolean
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { FullyFormedPayloadInterface } from '@standardnotes/models'
|
||||
import { SyncOptions } from './SyncOptions'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
|
||||
export interface SyncServiceInterface {
|
||||
export interface SyncServiceInterface extends AbstractService<SyncEvent> {
|
||||
sync(options?: Partial<SyncOptions>): Promise<unknown>
|
||||
resetSyncState(): void
|
||||
markAllItemsAsNeedingSyncAndPersist(): Promise<void>
|
||||
@@ -11,4 +13,5 @@ export interface SyncServiceInterface {
|
||||
persistPayloads(payloads: FullyFormedPayloadInterface[]): Promise<void>
|
||||
lockSyncing(): void
|
||||
unlockSyncing(): void
|
||||
syncSharedVaultsFromScratch(sharedVaultUuids: string[]): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,48 @@
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
import { Either, UserRequestType } from '@standardnotes/common'
|
||||
import { DeinitSource } from '../Application/DeinitSource'
|
||||
import { UserRegistrationResponseBody } from '@standardnotes/api'
|
||||
import { HttpError, HttpResponse, SignInResponse } from '@standardnotes/responses'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
|
||||
export interface UserClientInterface {
|
||||
export type CredentialsChangeFunctionResponse = { error?: HttpError }
|
||||
|
||||
export enum AccountEvent {
|
||||
SignedInOrRegistered = 'SignedInOrRegistered',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
|
||||
export interface SignedInOrRegisteredEventPayload {
|
||||
ephemeral: boolean
|
||||
mergeLocal: boolean
|
||||
awaitSync: boolean
|
||||
checkIntegrity: boolean
|
||||
}
|
||||
|
||||
export interface SignedOutEventPayload {
|
||||
source: DeinitSource
|
||||
}
|
||||
|
||||
export interface AccountEventData {
|
||||
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
|
||||
}
|
||||
|
||||
export interface UserClientInterface extends AbstractService<AccountEvent, AccountEventData> {
|
||||
isSignedIn(): boolean
|
||||
register(
|
||||
email: string,
|
||||
password: string,
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
): Promise<UserRegistrationResponseBody>
|
||||
signIn(
|
||||
email: string,
|
||||
password: string,
|
||||
strict: boolean,
|
||||
ephemeral: boolean,
|
||||
mergeLocal: boolean,
|
||||
awaitSync: boolean,
|
||||
): Promise<HttpResponse<SignInResponse>>
|
||||
deleteAccount(): Promise<{
|
||||
error: boolean
|
||||
message?: string
|
||||
@@ -10,4 +50,9 @@ export interface UserClientInterface {
|
||||
signOut(force?: boolean, source?: DeinitSource): Promise<void>
|
||||
submitUserRequest(requestType: UserRequestType): Promise<boolean>
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
updateAccountWithFirstTimeKeyPair(): Promise<{
|
||||
success?: true
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { HttpResponse, SignInResponse, User, HttpError, isErrorResponse } from '@standardnotes/responses'
|
||||
import { Either, KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
|
||||
import { HttpResponse, SignInResponse, User, isErrorResponse } from '@standardnotes/responses'
|
||||
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
|
||||
import {
|
||||
AccountEventData,
|
||||
AccountEvent,
|
||||
SignedInOrRegisteredEventPayload,
|
||||
CredentialsChangeFunctionResponse,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
import * as Messages from '../Strings/Messages'
|
||||
import { InfoStrings } from '../Strings/InfoStrings'
|
||||
@@ -28,28 +34,6 @@ import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterf
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
|
||||
export type CredentialsChangeFunctionResponse = { error?: HttpError }
|
||||
|
||||
export enum AccountEvent {
|
||||
SignedInOrRegistered = 'SignedInOrRegistered',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
|
||||
export interface SignedInOrRegisteredEventPayload {
|
||||
ephemeral: boolean
|
||||
mergeLocal: boolean
|
||||
awaitSync: boolean
|
||||
checkIntegrity: boolean
|
||||
}
|
||||
|
||||
export interface SignedOutEventPayload {
|
||||
source: DeinitSource
|
||||
}
|
||||
|
||||
export interface AccountEventData {
|
||||
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
|
||||
}
|
||||
|
||||
export class UserService
|
||||
extends AbstractService<AccountEvent, AccountEventData>
|
||||
implements UserClientInterface, InternalEventHandlerInterface
|
||||
@@ -125,6 +109,10 @@ export class UserService
|
||||
;(this.userApiService as unknown) = undefined
|
||||
}
|
||||
|
||||
isSignedIn(): boolean {
|
||||
return this.sessionManager.isSignedIn()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mergeLocal Whether to merge existing offline data into account. If false,
|
||||
* any pre-existing data will be fully deleted upon success.
|
||||
@@ -352,6 +340,20 @@ export class UserService
|
||||
}
|
||||
}
|
||||
|
||||
async updateAccountWithFirstTimeKeyPair(): Promise<{
|
||||
success?: true
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}> {
|
||||
if (!this.sessionManager.isUserMissingKeyPair()) {
|
||||
throw Error('Cannot update account with first time keypair if user already has a keypair')
|
||||
}
|
||||
|
||||
const result = await this.performProtocolUpgrade()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public async performProtocolUpgrade(): Promise<{
|
||||
success?: true
|
||||
canceled?: true
|
||||
@@ -524,7 +526,7 @@ export class UserService
|
||||
private async rewriteItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.itemManager.getDisplayableItemsKeys()
|
||||
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
||||
await this.storageService.forceDeletePayloads(payloads)
|
||||
await this.storageService.deletePayloads(payloads)
|
||||
await this.syncService.persistPayloads(payloads)
|
||||
}
|
||||
|
||||
@@ -571,7 +573,7 @@ export class UserService
|
||||
|
||||
const user = this.sessionManager.getUser() as User
|
||||
const currentEmail = user.email
|
||||
const rootKeys = await this.recomputeRootKeysForCredentialChange({
|
||||
const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({
|
||||
currentPassword: parameters.currentPassword,
|
||||
currentEmail,
|
||||
origination: parameters.origination,
|
||||
@@ -583,8 +585,8 @@ export class UserService
|
||||
|
||||
/** Now, change the credentials on the server. Roll back on failure */
|
||||
const { response } = await this.sessionManager.changeCredentials({
|
||||
currentServerPassword: rootKeys.currentRootKey.serverPassword as string,
|
||||
newRootKey: rootKeys.newRootKey,
|
||||
currentServerPassword: currentRootKey.serverPassword as string,
|
||||
newRootKey: newRootKey,
|
||||
wrappingKey,
|
||||
newEmail: parameters.newEmail,
|
||||
})
|
||||
@@ -596,7 +598,7 @@ export class UserService
|
||||
}
|
||||
|
||||
const rollback = await this.protocolService.createNewItemsKeyWithRollback()
|
||||
await this.protocolService.reencryptItemsKeys()
|
||||
await this.protocolService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this.syncService.sync({ awaitAll: true })
|
||||
|
||||
const defaultItemsKey = this.protocolService.getSureDefaultItemsKey()
|
||||
@@ -604,11 +606,11 @@ export class UserService
|
||||
|
||||
if (!itemsKeyWasSynced) {
|
||||
await this.sessionManager.changeCredentials({
|
||||
currentServerPassword: rootKeys.newRootKey.serverPassword as string,
|
||||
newRootKey: rootKeys.currentRootKey,
|
||||
currentServerPassword: newRootKey.serverPassword as string,
|
||||
newRootKey: currentRootKey,
|
||||
wrappingKey,
|
||||
})
|
||||
await this.protocolService.reencryptItemsKeys()
|
||||
await this.protocolService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await rollback()
|
||||
await this.syncService.sync({ awaitAll: true })
|
||||
|
||||
|
||||
38
packages/services/src/Domain/UserEvent/UserEventService.ts
Normal file
38
packages/services/src/Domain/UserEvent/UserEventService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { UserEventServerHash } from '@standardnotes/responses'
|
||||
import { SyncEvent, SyncEventReceivedUserEventsData } from '../Event/SyncEvent'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { UserEventServiceEventPayload, UserEventServiceEvent } from './UserEventServiceEvent'
|
||||
|
||||
export class UserEventService
|
||||
extends AbstractService<UserEventServiceEvent, UserEventServiceEventPayload>
|
||||
implements InternalEventHandlerInterface
|
||||
{
|
||||
constructor(internalEventBus: InternalEventBusInterface) {
|
||||
super(internalEventBus)
|
||||
|
||||
internalEventBus.addEventHandler(this, SyncEvent.ReceivedUserEvents)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SyncEvent.ReceivedUserEvents) {
|
||||
return this.handleReceivedUserEvents(event.payload as SyncEventReceivedUserEventsData)
|
||||
}
|
||||
}
|
||||
|
||||
private async handleReceivedUserEvents(userEvents: UserEventServerHash[]): Promise<void> {
|
||||
if (userEvents.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const serverEvent of userEvents) {
|
||||
const serviceEvent: UserEventServiceEventPayload = {
|
||||
eventPayload: JSON.parse(serverEvent.event_payload),
|
||||
}
|
||||
|
||||
await this.notifyEventSync(UserEventServiceEvent.UserEventReceived, serviceEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UserEventPayload } from '@standardnotes/responses'
|
||||
|
||||
export enum UserEventServiceEvent {
|
||||
UserEventReceived = 'UserEventReceived',
|
||||
}
|
||||
|
||||
export type UserEventServiceEventPayload = {
|
||||
eventPayload: UserEventPayload
|
||||
}
|
||||
10
packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts
Normal file
10
packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { KeySystemRootKeyPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models'
|
||||
|
||||
export type ChangeVaultOptionsDTO = {
|
||||
vault: VaultListingInterface
|
||||
newPasswordType:
|
||||
| { passwordType: KeySystemRootKeyPasswordType.Randomized }
|
||||
| { passwordType: KeySystemRootKeyPasswordType.UserInputted; userInputtedPassword: string }
|
||||
| undefined
|
||||
newKeyStorageMode: KeySystemRootKeyStorageMode | undefined
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services'
|
||||
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
|
||||
import {
|
||||
KeySystemRootKeyPasswordType,
|
||||
KeySystemRootKeyStorageMode,
|
||||
VaultListingInterface,
|
||||
VaultListingMutator,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionProviderInterface, KeySystemKeyManagerInterface } from '@standardnotes/encryption'
|
||||
import { ChangeVaultOptionsDTO } from '../ChangeVaultOptionsDTO'
|
||||
import { GetVaultUseCase } from './GetVault'
|
||||
import { assert } from '@standardnotes/utils'
|
||||
|
||||
export class ChangeVaultKeyOptionsUseCase {
|
||||
constructor(
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
) {}
|
||||
|
||||
private get keys(): KeySystemKeyManagerInterface {
|
||||
return this.encryption.keys
|
||||
}
|
||||
|
||||
async execute(dto: ChangeVaultOptionsDTO): Promise<void> {
|
||||
const useStorageMode = dto.newKeyStorageMode ?? dto.vault.keyStorageMode
|
||||
|
||||
if (dto.newPasswordType) {
|
||||
if (dto.vault.keyPasswordType === dto.newPasswordType.passwordType) {
|
||||
throw new Error('Vault password type is already set to this type')
|
||||
}
|
||||
|
||||
if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.UserInputted) {
|
||||
if (!dto.newPasswordType.userInputtedPassword) {
|
||||
throw new Error('User inputted password is required')
|
||||
}
|
||||
await this.changePasswordTypeToUserInputted(dto.vault, dto.newPasswordType.userInputtedPassword, useStorageMode)
|
||||
} else if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.Randomized) {
|
||||
await this.changePasswordTypeToRandomized(dto.vault, useStorageMode)
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.newKeyStorageMode) {
|
||||
const usecase = new GetVaultUseCase(this.items)
|
||||
const latestVault = usecase.execute({ keySystemIdentifier: dto.vault.systemIdentifier })
|
||||
assert(latestVault)
|
||||
|
||||
if (latestVault.rootKeyParams.passwordType !== KeySystemRootKeyPasswordType.UserInputted) {
|
||||
throw new Error('Vault uses randomized password and cannot change its storage preference')
|
||||
}
|
||||
|
||||
if (dto.newKeyStorageMode === latestVault.keyStorageMode) {
|
||||
throw new Error('Vault already uses this storage preference')
|
||||
}
|
||||
|
||||
if (
|
||||
dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Local ||
|
||||
dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Ephemeral
|
||||
) {
|
||||
await this.changeStorageModeToLocalOrEphemeral(latestVault, dto.newKeyStorageMode)
|
||||
} else if (dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.changeStorageModeToSynced(latestVault)
|
||||
}
|
||||
}
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async changePasswordTypeToUserInputted(
|
||||
vault: VaultListingInterface,
|
||||
userInputtedPassword: string,
|
||||
storageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
const newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
|
||||
systemIdentifier: vault.systemIdentifier,
|
||||
userInputtedPassword: userInputtedPassword,
|
||||
})
|
||||
|
||||
if (storageMode === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
} else {
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, storageMode)
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.rootKeyParams = newRootKey.keyParams
|
||||
})
|
||||
|
||||
await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier)
|
||||
}
|
||||
|
||||
private async changePasswordTypeToRandomized(
|
||||
vault: VaultListingInterface,
|
||||
storageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
const newRootKey = this.encryption.createRandomizedKeySystemRootKey({
|
||||
systemIdentifier: vault.systemIdentifier,
|
||||
})
|
||||
|
||||
if (storageMode !== KeySystemRootKeyStorageMode.Synced) {
|
||||
throw new Error('Cannot change to randomized password if root key storage is not synced')
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.rootKeyParams = newRootKey.keyParams
|
||||
})
|
||||
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
|
||||
await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier)
|
||||
}
|
||||
|
||||
private async changeStorageModeToLocalOrEphemeral(
|
||||
vault: VaultListingInterface,
|
||||
newKeyStorageMode: KeySystemRootKeyStorageMode,
|
||||
): Promise<void> {
|
||||
const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier)
|
||||
if (!primaryKey) {
|
||||
throw new Error('No primary key found')
|
||||
}
|
||||
|
||||
this.keys.intakeNonPersistentKeySystemRootKey(primaryKey, newKeyStorageMode)
|
||||
await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier)
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.keyStorageMode = newKeyStorageMode
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async changeStorageModeToSynced(vault: VaultListingInterface): Promise<void> {
|
||||
const allRootKeys = this.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
const syncedRootKeys = this.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier)
|
||||
|
||||
for (const key of allRootKeys) {
|
||||
const existingSyncedKey = syncedRootKeys.find((syncedKey) => syncedKey.token === key.token)
|
||||
if (existingSyncedKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.mutator.insertItem(key)
|
||||
}
|
||||
|
||||
await this.mutator.changeItem<VaultListingMutator>(vault, (mutator) => {
|
||||
mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced
|
||||
})
|
||||
}
|
||||
}
|
||||
115
packages/services/src/Domain/Vaults/UseCase/CreateVault.ts
Normal file
115
packages/services/src/Domain/Vaults/UseCase/CreateVault.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
|
||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import {
|
||||
KeySystemRootKeyParamsInterface,
|
||||
KeySystemRootKeyPasswordType,
|
||||
VaultListingContentSpecialized,
|
||||
VaultListingInterface,
|
||||
KeySystemRootKeyStorageMode,
|
||||
FillItemContentSpecialized,
|
||||
KeySystemRootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface'
|
||||
|
||||
export class CreateVaultUseCase {
|
||||
constructor(
|
||||
private mutator: MutatorClientInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: {
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
const keySystemIdentifier = UuidGenerator.GenerateUuid()
|
||||
|
||||
const rootKey = await this.createKeySystemRootKey({
|
||||
keySystemIdentifier,
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
storagePreference: dto.storagePreference,
|
||||
})
|
||||
|
||||
await this.createKeySystemItemsKey(keySystemIdentifier, rootKey.token)
|
||||
|
||||
const vaultListing = await this.createVaultListing({
|
||||
keySystemIdentifier,
|
||||
vaultName: dto.vaultName,
|
||||
vaultDescription: dto.vaultDescription,
|
||||
passwordType: dto.userInputtedPassword
|
||||
? KeySystemRootKeyPasswordType.UserInputted
|
||||
: KeySystemRootKeyPasswordType.Randomized,
|
||||
rootKeyParams: rootKey.keyParams,
|
||||
storage: dto.storagePreference,
|
||||
})
|
||||
|
||||
await this.sync.sync()
|
||||
|
||||
return vaultListing
|
||||
}
|
||||
|
||||
private async createVaultListing(dto: {
|
||||
keySystemIdentifier: string
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
passwordType: KeySystemRootKeyPasswordType
|
||||
rootKeyParams: KeySystemRootKeyParamsInterface
|
||||
storage: KeySystemRootKeyStorageMode
|
||||
}): Promise<VaultListingInterface> {
|
||||
const content: VaultListingContentSpecialized = {
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
rootKeyParams: dto.rootKeyParams,
|
||||
keyStorageMode: dto.storage,
|
||||
name: dto.vaultName,
|
||||
description: dto.vaultDescription,
|
||||
}
|
||||
|
||||
return this.mutator.createItem(ContentType.VaultListing, FillItemContentSpecialized(content), true)
|
||||
}
|
||||
|
||||
private async createKeySystemItemsKey(keySystemIdentifier: string, rootKeyToken: string): Promise<void> {
|
||||
const keySystemItemsKey = this.encryption.createKeySystemItemsKey(
|
||||
UuidGenerator.GenerateUuid(),
|
||||
keySystemIdentifier,
|
||||
undefined,
|
||||
rootKeyToken,
|
||||
)
|
||||
|
||||
await this.mutator.insertItem(keySystemItemsKey)
|
||||
}
|
||||
|
||||
private async createKeySystemRootKey(dto: {
|
||||
keySystemIdentifier: string
|
||||
vaultName: string
|
||||
vaultDescription?: string
|
||||
userInputtedPassword: string | undefined
|
||||
storagePreference: KeySystemRootKeyStorageMode
|
||||
}): Promise<KeySystemRootKeyInterface> {
|
||||
let newRootKey: KeySystemRootKeyInterface | undefined
|
||||
|
||||
if (dto.userInputtedPassword) {
|
||||
newRootKey = this.encryption.createUserInputtedKeySystemRootKey({
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
userInputtedPassword: dto.userInputtedPassword,
|
||||
})
|
||||
} else {
|
||||
newRootKey = this.encryption.createRandomizedKeySystemRootKey({
|
||||
systemIdentifier: dto.keySystemIdentifier,
|
||||
})
|
||||
}
|
||||
|
||||
if (dto.storagePreference === KeySystemRootKeyStorageMode.Synced) {
|
||||
await this.mutator.insertItem(newRootKey, true)
|
||||
} else {
|
||||
this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, dto.storagePreference)
|
||||
}
|
||||
|
||||
return newRootKey
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user