tests: vault tests [no ci]

This commit is contained in:
Mo 2023-07-26 06:43:52 -05:00
parent 908cbaf895
commit 7c463b36af
No known key found for this signature in database
GPG Key ID: 034B13B1644297E1
12 changed files with 279 additions and 159 deletions

View File

@ -22,8 +22,6 @@ import {
import { HandleRootKeyChangedMessage } from './UseCase/HandleRootKeyChangedMessage'
import { SessionEvent } from '../Session/SessionEvent'
import { AsymmetricMessageServer } from '@standardnotes/api'
import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData'
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { GetOutboundMessages } from './UseCase/GetOutboundMessages'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
import { GetVault } from '../Vault/UseCase/GetVault'
@ -32,7 +30,6 @@ import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { FindContact } from '../Contacts/UseCase/FindContact'
import { CreateOrEditContact } from '../Contacts/UseCase/CreateOrEditContact'
import { ReplaceContactData } from '../Contacts/UseCase/ReplaceContactData'
import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
export class AsymmetricMessageService
@ -46,12 +43,10 @@ export class AsymmetricMessageService
private messageServer: AsymmetricMessageServer,
private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _replaceContactData: ReplaceContactData,
private _getTrustedPayload: GetTrustedPayload,
private _getVault: GetVault,
private _handleRootKeyChangedMessage: HandleRootKeyChangedMessage,
private _sendOwnContactChangedMessage: SendOwnContactChangeMessage,
private _getOutboundMessagesUseCase: GetOutboundMessages,
private _getInboundMessagesUseCase: GetInboundMessages,
private _getUntrustedPayload: GetUntrustedPayload,
@ -60,11 +55,26 @@ export class AsymmetricMessageService
super(eventBus)
}
public override deinit(): void {
super.deinit()
;(this.messageServer as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.mutator as unknown) = undefined
;(this._createOrEditContact as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._replaceContactData as unknown) = undefined
;(this._getTrustedPayload as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._handleRootKeyChangedMessage as unknown) = undefined
;(this._getOutboundMessagesUseCase as unknown) = undefined
;(this._getInboundMessagesUseCase as unknown) = undefined
;(this._getUntrustedPayload as unknown) = undefined
}
async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) {
case SessionEvent.UserKeyPairChanged:
void this.messageServer.deleteAllInboundMessages()
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
break
case SyncEvent.ReceivedAsymmetricMessages:
void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData)
@ -89,31 +99,6 @@ export class AsymmetricMessageService
await this.handleRemoteReceivedAsymmetricMessages(messages)
}
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.previous) {
return
}
const contacts = this._getAllContacts.execute()
if (contacts.isFailed()) {
return
}
for (const contact of contacts.getValue()) {
if (contact.isMe) {
continue
}
await this._sendOwnContactChangedMessage.execute({
senderOldKeyPair: data.previous.encryption,
senderOldSigningKeyPair: data.previous.signing,
senderNewKeyPair: data.current.encryption,
senderNewSigningKeyPair: data.current.signing,
contact,
})
}
}
sortServerMessages(messages: AsymmetricMessageServerHash[]): AsymmetricMessageServerHash[] {
const SortedPriorityTypes = [AsymmetricMessagePayloadType.SenderKeypairChanged]
@ -288,22 +273,4 @@ export class AsymmetricMessageService
): Promise<void> {
await this._handleRootKeyChangedMessage.execute(trustedPayload)
}
public override deinit(): void {
super.deinit()
;(this.messageServer as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.mutator as unknown) = undefined
;(this._createOrEditContact as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
;(this._replaceContactData as unknown) = undefined
;(this._getTrustedPayload as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._handleRootKeyChangedMessage as unknown) = undefined
;(this._sendOwnContactChangedMessage as unknown) = undefined
;(this._getOutboundMessagesUseCase as unknown) = undefined
;(this._getInboundMessagesUseCase as unknown) = undefined
;(this._getUntrustedPayload as unknown) = undefined
}
}

View File

@ -32,7 +32,7 @@ export class ResendAllMessages implements UseCaseInterface<void> {
for (const message of messages.data.messages) {
const recipient = this.findContact.execute({ userUuid: message.user_uuid })
if (recipient) {
if (recipient.isFailed()) {
errors.push(`Contact not found for invite ${message.user_uuid}`)
continue
}
@ -41,7 +41,7 @@ export class ResendAllMessages implements UseCaseInterface<void> {
keys: params.keys,
previousKeys: params.previousKeys,
message: message,
recipient,
recipient: recipient.getValue(),
})
await this.messageServer.deleteMessage({

View File

@ -1,3 +1,4 @@
import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage'
import { DeleteContact } from './UseCase/DeleteContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
@ -42,6 +43,7 @@ export class ContactService
private _createOrEditContact: CreateOrEditContact,
private _editContact: EditContact,
private _validateItemSigner: ValidateItemSigner,
private _sendOwnContactChangedMessage: SendOwnContactChangeMessage,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
@ -52,11 +54,36 @@ export class ContactService
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SessionEvent.UserKeyPairChanged) {
const data = event.payload as UserKeyPairChangedEventData
await this.selfContactManager.updateWithNewPublicKeySet({
encryption: data.current.encryption.publicKey,
signing: data.current.signing.publicKey,
})
void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData)
}
}
private async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise<void> {
if (!data.previous) {
return
}
const contacts = this._getAllContacts.execute()
if (contacts.isFailed()) {
return
}
for (const contact of contacts.getValue()) {
if (contact.isMe) {
continue
}
await this._sendOwnContactChangedMessage.execute({
senderOldKeyPair: data.previous.encryption,
senderOldSigningKeyPair: data.previous.signing,
senderNewKeyPair: data.current.encryption,
senderNewSigningKeyPair: data.current.signing,
contact,
})
}
}

View File

@ -5,7 +5,7 @@ import {
AsymmetricMessageSenderKeypairChanged,
} from '@standardnotes/models'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendMessage } from './SendMessage'
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'

View File

@ -112,7 +112,6 @@ export class ValidateItemSigner {
const signerPublicKey = signatureResult.publicKey
const trustedContact = this.findContact.execute({ signingPublicKey: signerPublicKey })
if (trustedContact.isFailed()) {
return ItemSignatureValidationResult.NotTrusted
}

View File

@ -21,7 +21,7 @@ export * from './AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite'
export * from './AsymmetricMessage/UseCase/ResendAllMessages'
export * from './AsymmetricMessage/UseCase/ResendMessage'
export * from './AsymmetricMessage/UseCase/SendMessage'
export * from './AsymmetricMessage/UseCase/SendOwnContactChangeMessage'
export * from './Contacts/UseCase/SendOwnContactChangeMessage'
export * from './Auth/AuthClientInterface'
export * from './Auth/AuthManager'
export * from './Authenticator/AuthenticatorClientInterface'

View File

@ -702,12 +702,10 @@ export class Dependencies {
this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.CreateOrEditContact),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
this.get(TYPES.ReplaceContactData),
this.get(TYPES.GetTrustedPayload),
this.get(TYPES.GetVault),
this.get(TYPES.HandleRootKeyChangedMessage),
this.get(TYPES.SendOwnContactChangeMessage),
this.get(TYPES.GetOutboundMessages),
this.get(TYPES.GetInboundMessages),
this.get(TYPES.GetUntrustedPayload),
@ -791,6 +789,7 @@ export class Dependencies {
this.get(TYPES.CreateOrEditContact),
this.get(TYPES.EditContact),
this.get(TYPES.ValidateItemSigner),
this.get(TYPES.SendOwnContactChangeMessage),
this.get(TYPES.InternalEventBus),
)
})

View File

@ -358,64 +358,39 @@ export class AppContext {
})
}
resolveWhenAsymmetricMessageProcessingCompletes() {
resolveWhenAsyncFunctionCompletes(object, functionName) {
return new Promise((resolve) => {
const objectToSpy = this.asymmetric
sinon.stub(objectToSpy, 'handleRemoteReceivedAsymmetricMessages').callsFake(async (messages) => {
objectToSpy.handleRemoteReceivedAsymmetricMessages.restore()
const result = await objectToSpy.handleRemoteReceivedAsymmetricMessages(messages)
sinon.stub(object, functionName).callsFake(async (params) => {
object[functionName].restore()
const result = await object[functionName](params)
resolve()
return result
})
})
}
resolveWhenAsymmetricMessageProcessingCompletes() {
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
}
resolveWhenUserMessagesProcessingCompletes() {
return new Promise((resolve) => {
const objectToSpy = this.application.dependencies.get(TYPES.UserEventService)
sinon.stub(objectToSpy, 'handleReceivedUserEvents').callsFake(async (params) => {
objectToSpy.handleReceivedUserEvents.restore()
const result = await objectToSpy.handleReceivedUserEvents(params)
resolve()
return result
})
})
const objectToSpy = this.application.dependencies.get(TYPES.UserEventService)
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'handleReceivedUserEvents')
}
resolveWhenAllInboundAsymmetricMessagesAreDeleted() {
return new Promise((resolve) => {
const objectToSpy = this.application.dependencies.get(TYPES.AsymmetricMessageServer)
sinon.stub(objectToSpy, 'deleteAllInboundMessages').callsFake(async (params) => {
objectToSpy.deleteAllInboundMessages.restore()
const result = await objectToSpy.deleteAllInboundMessages(params)
resolve()
return result
})
})
const objectToSpy = this.application.dependencies.get(TYPES.AsymmetricMessageServer)
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'deleteAllInboundMessages')
}
resolveWhenAllInboundSharedVaultInvitesAreDeleted() {
return new Promise((resolve) => {
const objectToSpy = this.application.vaultInvites.invitesServer
sinon.stub(objectToSpy, 'deleteAllInboundInvites').callsFake(async (params) => {
objectToSpy.deleteAllInboundInvites.restore()
const result = await objectToSpy.deleteAllInboundInvites(params)
resolve()
return result
})
})
const objectToSpy = this.application.vaultInvites.invitesServer
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'deleteAllInboundInvites')
}
resolveWhenSharedVaultServiceSendsContactShareMessage() {
return new Promise((resolve) => {
const objectToSpy = this.sharedVaults
sinon.stub(objectToSpy, 'shareContactWithVaults').callsFake(async (contact) => {
objectToSpy.shareContactWithVaults.restore()
const result = await objectToSpy.shareContactWithVaults(contact)
resolve()
return result
})
})
const objectToSpy = this.sharedVaults
return this.resolveWhenAsyncFunctionCompletes(objectToSpy, 'shareContactWithVaults')
}
resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) {

View File

@ -69,7 +69,7 @@ describe('asymmetric messages', function () {
},
}
await service.sendOwnContactChangeEventToAllContacts(eventData)
await context.contacts.sendOwnContactChangeEventToAllContacts(eventData)
const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing')
@ -239,14 +239,20 @@ describe('asymmetric messages', function () {
it('should send sender keypair changed message to trusted contacts', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const sendPromise = context.resolveWhenAsyncFunctionCompletes(
context.contacts,
'sendOwnContactChangeEventToAllContacts',
)
await context.changePassword('new password')
await sendPromise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing()
await contactContext.sync()
await completedProcessingMessagesPromise

View File

@ -21,59 +21,99 @@ describe('vault key management', function () {
await context.launch()
})
it('should lock non-persistent vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
describe('locking', () => {
it('should throw if attempting to add item to locked vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
await context.vaultLocks.lockNonPersistentVault(vault)
const item = await context.createSyncedNote('test note', 'test note text')
await Factory.expectThrowsAsync(
() => context.vaults.moveItemToVault(vault, item),
'Attempting to add item to locked vault',
)
})
await context.vaultLocks.lockNonPersistentVault(vault)
it('should throw if attempting to remove item from locked vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
expect(context.vaultLocks.isVaultLocked(vault)).to.be.true
})
const item = await context.createSyncedNote('test note', 'test note text')
it('should not be able to lock user-inputted vault with synced key', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Synced,
await context.vaults.moveItemToVault(vault, item)
await context.vaultLocks.lockNonPersistentVault(vault)
await Factory.expectThrowsAsync(
() => context.vaults.removeItemFromVault(item),
'Attempting to remove item from locked vault',
)
})
await Factory.expectThrowsAsync(
() => context.vaultLocks.lockNonPersistentVault(vault),
'Vault uses synced key storage and cannot be locked',
)
})
it('should lock non-persistent vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
it('should not be able to lock randomized vault', async () => {
const vault = await context.vaults.createRandomizedVault({
name: 'test vault',
description: 'test vault description',
await context.vaultLocks.lockNonPersistentVault(vault)
expect(context.vaultLocks.isVaultLocked(vault)).to.be.true
})
await Factory.expectThrowsAsync(
() => context.vaultLocks.lockNonPersistentVault(vault),
'Vault uses synced key storage and cannot be locked',
)
})
it('should not be able to lock user-inputted vault with synced key', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
it('should throw if attempting to change password of locked vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
await Factory.expectThrowsAsync(
() => context.vaultLocks.lockNonPersistentVault(vault),
'Vault uses synced key storage and cannot be locked',
)
})
await context.vaultLocks.lockNonPersistentVault(vault)
it('should not be able to lock randomized vault', async () => {
const vault = await context.vaults.createRandomizedVault({
name: 'test vault',
description: 'test vault description',
})
await Factory.expectThrowsAsync(
() => context.vaults.changeVaultOptions({ vault }),
'Attempting to change vault options on a locked vault',
)
await Factory.expectThrowsAsync(
() => context.vaultLocks.lockNonPersistentVault(vault),
'Vault uses synced key storage and cannot be locked',
)
})
it('should throw if attempting to change password of locked vault', async () => {
const vault = await context.vaults.createUserInputtedPasswordVault({
name: 'test vault',
description: 'test vault description',
userInputtedPassword: 'test password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
await context.vaultLocks.lockNonPersistentVault(vault)
await Factory.expectThrowsAsync(
() => context.vaults.changeVaultOptions({ vault }),
'Attempting to change vault options on a locked vault',
)
})
})
describe('key rotation and persistence', () => {

View File

@ -24,18 +24,139 @@ describe('keypair change', function () {
})
it('contacts should be able to handle receiving multiple keypair changed messages and trust them in order', async () => {
console.error('TODO: implement test')
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
const publicKeyChain = []
const signingPublicKeyChain = []
publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey)
await context.changePassword('new_password')
publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey)
await context.changePassword('new_password-2')
publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey)
await context.changePassword('new_password-3')
publicKeyChain.push(context.publicKey)
signingPublicKeyChain.push(context.signingPublicKey)
await context.changeNoteTitleAndSync(note, 'new title')
contactContext.unlockSyncing()
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
const originatorContact = contactContext.contacts.findContact(context.userUuid)
let currentKeySet = originatorContact.publicKeySet
for (let i = publicKeyChain.length - 1; i >= 0; i--) {
const publicKey = publicKeyChain[i]
const signingPublicKey = signingPublicKeyChain[i]
expect(currentKeySet.encryption).to.equal(publicKey)
expect(currentKeySet.signing).to.equal(signingPublicKey)
currentKeySet = currentKeySet.previousKeySet
}
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote.title).to.equal('new title')
expect(receivedNote.signatureData.required).to.be.true
expect(receivedNote.signatureData.result.passes).to.be.true
await deinitContactContext()
})
it('should not trust messages sent with previous key pair', async () => {
console.error('TODO: implement test')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
const previousKeyPair = context.encryption.getKeyPair()
const previousSigningKeyPair = context.encryption.getSigningKeyPair()
await context.changePassword('new_password')
sinon.stub(context.encryption, 'getKeyPair').returns(previousKeyPair)
sinon.stub(context.encryption, 'getSigningKeyPair').returns(previousSigningKeyPair)
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'New Name',
description: 'New Description',
})
contactContext.unlockSyncing()
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal(sharedVault.name)
expect(updatedVault.description).to.equal(sharedVault.description)
expect(updatedVault.name).to.not.equal('New Name')
expect(updatedVault.description).to.not.equal('New Description')
await deinitContactContext()
})
it('should reupload invites after rotating keypair', async () => {
console.error('TODO: implement test')
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
contactContext.lockSyncing()
const invite = (await contactContext.vaultInvites.downloadInboundInvites())[0]
const promise = context.resolveWhenAsyncFunctionCompletes(
context.application.dependencies.get(TYPES.SendVaultInvite),
'execute',
)
await context.changePassword('new_password')
await promise
const updatedInvite = (await contactContext.vaultInvites.downloadInboundInvites())[0]
expect(updatedInvite.uuid).to.not.equal(invite.uuid)
expect(updatedInvite.created_at_timestamp).to.not.equal(invite.created_at_timestamp)
await deinitContactContext()
})
it('should reupload asymmetric messages after rotating keypair', async () => {
console.error('TODO: implement test')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'New Name',
description: 'New Description',
})
const originalMessages = await contactContext.asymmetric.getInboundMessages()
expect(originalMessages.length).to.equal(1)
const originalMessage = originalMessages[0]
const promise = context.resolveWhenAsyncFunctionCompletes(
context.application.dependencies.get(TYPES.HandleKeyPairChange),
'execute',
)
await context.changePassword('new_password')
await promise
const updatedMessages = await contactContext.asymmetric.getInboundMessages()
const expectedMessages = ['keypair-change', 'vault-change']
expect(updatedMessages.length).to.equal(expectedMessages.length)
expect(updatedMessages.some((message) => message.uuid === originalMessage.uuid)).to.be.false
expect(updatedMessages.some((message) => message.created_at_timestamp === originalMessage.created_at_timestamp)).to
.be.false
await deinitContactContext()
})
})

View File

@ -24,20 +24,6 @@ describe('vaults', function () {
vaults = context.vaults
})
describe('locking', () => {
it('should throw if attempting to add item to locked vault', async () => {
console.error('TODO: implement')
})
it('should throw if attempting to remove item from locked vault', async () => {
console.error('TODO: implement')
})
it('locking vault should remove root key and items keys from memory', async () => {
console.error('TODO: implement')
})
})
describe('offline', function () {
it('should be able to create an offline vault', async () => {
const vault = await vaults.createRandomizedVault({