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

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

View File

@@ -0,0 +1,8 @@
export const Version1CollaborationId = '1'
export type CollaborationIDData = {
version: string
userUuid: string
publicKey: string
signingPublicKey: string
}

View 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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -0,0 +1 @@
export const UnknownContactName = 'Unnamed contact'

View File

@@ -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
}
}

View File

@@ -0,0 +1 @@
export type FindContactQuery = { userUuid: string } | { signingPublicKey: string } | { publicKey: string }

View File

@@ -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')
}
}

View File

@@ -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
}
}

View File

@@ -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')
})
})
})
})
})

View File

@@ -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'
}
}

View File

@@ -0,0 +1 @@
export type ValidateItemSignerResult = 'not-applicable' | 'yes' | 'no'