internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
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'
|
||||
Reference in New Issue
Block a user