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,277 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('asymmetric messages', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let service
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
service = context.asymmetric
})
it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => {
console.error('TODO: implement')
})
it('should delete message after processing it', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const eventData = {
oldKeyPair: context.encryption.getKeyPair(),
oldSigningKeyPair: context.encryption.getSigningKeyPair(),
newKeyPair: context.encryption.getKeyPair(),
newSigningKeyPair: context.encryption.getSigningKeyPair(),
}
await service.sendOwnContactChangeEventToAllContacts(eventData)
const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
expect(deleteFunction.callCount).to.equal(1)
const messages = await contactContext.asymmetric.getInboundMessages()
expect(messages.length).to.equal(0)
await deinitContactContext()
})
it('should send contact share message after trusted contact belonging to group changes', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
await context.contacts.createOrEditTrustedContact({
contactUuid: thirdPartyContext.userUuid,
publicKey: thirdPartyContext.publicKey,
signingPublicKey: thirdPartyContext.signingPublicKey,
name: 'Changed 3rd Party Name',
})
await sendContactSharePromise
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid)
expect(updatedContact.name).to.equal('Changed 3rd Party Name')
await deinitContactContext()
await deinitThirdPartyContext()
})
it('should not send contact share message to self or to contact who is changed', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await Collaboration.acceptAllInvites(thirdPartyContext)
await contactContext.sync()
await handleInitialContactShareMessage
const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage()
await context.contacts.createOrEditTrustedContact({
contactUuid: thirdPartyContext.userUuid,
publicKey: thirdPartyContext.publicKey,
signingPublicKey: thirdPartyContext.signingPublicKey,
name: 'Changed 3rd Party Name',
})
await sendContactSharePromise
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedContactShareMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedContactShareMessage')
const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage')
await context.sync()
await contactContext.sync()
await thirdPartyContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
expect(thirdPartySpy.callCount).to.equal(0)
await deinitThirdPartyContext()
await deinitContactContext()
})
it('should send shared vault root key change message after root key change', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.vaults.rotateVaultRootKey(sharedVault)
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
await context.sync()
await contactContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
await deinitContactContext()
})
it('should send shared vault metadata change message after shared vault name change', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'New Name',
description: 'New Description',
})
const firstPartySpy = sinon.spy(context.asymmetric, 'handleVaultMetadataChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleVaultMetadataChangedMessage')
await context.sync()
await contactContext.sync()
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal('New Name')
expect(updatedVault.description).to.equal('New Description')
await deinitContactContext()
})
it('should send sender keypair changed message to trusted contacts', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.changePassword('new password')
const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const contact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(contact.publicKeySet.encryption).to.equal(context.publicKey)
expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey)
await deinitContactContext()
})
it('should process sender keypair changed message', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const originalContact = contactContext.contacts.findTrustedContact(context.userUuid)
await context.changePassword('new_password')
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption)
expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing)
expect(updatedContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(context.signingPublicKey)
await deinitContactContext()
})
it('sender keypair changed message should be signed using old key pair', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const oldKeyPair = context.encryption.getKeyPair()
const oldSigningKeyPair = context.encryption.getSigningKeyPair()
await context.changePassword('new password')
const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage')
await context.sync()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const message = secondPartySpy.args[0][0]
const encryptedMessage = message.encrypted_message
const publicKeySet =
contactContext.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString(encryptedMessage)
expect(publicKeySet.encryption).to.equal(oldKeyPair.publicKey)
expect(publicKeySet.signing).to.equal(oldSigningKeyPair.publicKey)
await deinitContactContext()
})
it('sender keypair changed message should contain new keypair and be trusted', async () => {
const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await context.changePassword('new password')
const newKeyPair = context.encryption.getKeyPair()
const newSigningKeyPair = context.encryption.getSigningKeyPair()
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey)
await deinitContactContext()
})
it('should delete all inbound messages after changing user password', async () => {
/** Messages to user are encrypted with old keypair and are no longer decryptable */
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,186 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault conflicts', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
it('after being removed from shared vault, attempting to sync previous vault item should result in SharedVaultNotMemberError. The item should be duplicated then removed.', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const promise = contactContext.resolveWithConflicts()
contactContext.unlockSyncing()
await contactContext.changeNoteTitleAndSync(note, 'new title')
const conflicts = await promise
await contactContext.sync()
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const collaboratorNotes = contactContext.items.getDisplayableNotes()
expect(collaboratorNotes.length).to.equal(1)
expect(collaboratorNotes[0].duplicate_of).to.not.be.undefined
expect(collaboratorNotes[0].title).to.equal('new title')
await deinitContactContext()
})
it('conflicts created should be associated with the vault', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changeNoteTitle(note, 'new title first client')
await contactContext.changeNoteTitle(note, 'new title second client')
const doneAddingConflictToSharedVault = contactContext.resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(
note.uuid,
)
await context.sync({ desc: 'First client sync' })
await contactContext.sync({
desc: 'Second client sync with conflicts to be created',
})
await doneAddingConflictToSharedVault
await context.sync({ desc: 'First client sync with conflicts to be pulled in' })
expect(context.items.invalidItems.length).to.equal(0)
expect(contactContext.items.invalidItems.length).to.equal(0)
const collaboratorNotes = contactContext.items.getDisplayableNotes()
expect(collaboratorNotes.length).to.equal(2)
expect(collaboratorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
const originatorNotes = context.items.getDisplayableNotes()
expect(originatorNotes.length).to.equal(2)
expect(originatorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined
await deinitContactContext()
})
it('attempting to modify note as read user should result in SharedVaultInsufficientPermissionsError', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Read)
const promise = contactContext.resolveWithConflicts()
await contactContext.changeNoteTitleAndSync(note, 'new title')
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultInsufficientPermissionsError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
await deinitContactContext()
})
it('should handle SharedVaultNotMemberError by duplicating item to user non-vault', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.changeNoteTitleAndSync(note, 'new title')
const notes = contactContext.notes
expect(notes.length).to.equal(1)
expect(notes[0].title).to.equal('new title')
expect(notes[0].key_system_identifier).to.not.be.ok
expect(notes[0].duplicate_of).to.equal(note.uuid)
await deinitContactContext()
})
it('attempting to save note to non-existent vault should result in SharedVaultNotMemberError conflict', async () => {
context.anticipateConsoleError(
'Error decrypting contentKey from parameters',
'An invalid shared vault uuid is being assigned to an item',
)
const { note } = await Collaboration.createSharedVaultWithNote(context)
const promise = context.resolveWithConflicts()
const objectToSpy = context.application.sync
sinon.stub(objectToSpy, 'payloadsByPreparingForServer').callsFake(async (params) => {
objectToSpy.payloadsByPreparingForServer.restore()
const payloads = await objectToSpy.payloadsByPreparingForServer(params)
for (const payload of payloads) {
payload.shared_vault_uuid = 'non-existent-vault-uuid-123'
}
return payloads
})
await context.changeNoteTitleAndSync(note, 'new-title')
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
})
it('should create a non-vaulted copy if attempting to move item from vault to user and item belongs to someone else', async () => {
const { note, sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = contactContext.resolveWithConflicts()
await contactContext.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const duplicateNote = contactContext.findDuplicateNote(note.uuid)
expect(duplicateNote).to.not.be.undefined
expect(duplicateNote.key_system_identifier).to.not.be.ok
const existingNote = contactContext.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
it('should created a non-vaulted copy if admin attempts to move item from vault to user if the item belongs to someone else', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await contactContext.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(contactContext, sharedVault, note)
await context.sync()
const promise = context.resolveWithConflicts()
await context.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
const duplicateNote = context.findDuplicateNote(note.uuid)
expect(duplicateNote).to.not.be.undefined
expect(duplicateNote.key_system_identifier).to.not.be.ok
const existingNote = context.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
})

View File

@@ -0,0 +1,83 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('contacts', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
it('should create contact', async () => {
const contact = await context.contacts.createOrEditTrustedContact({
name: 'John Doe',
publicKey: 'my_public_key',
signingPublicKey: 'my_signing_public_key',
contactUuid: '123',
})
expect(contact).to.not.be.undefined
expect(contact.name).to.equal('John Doe')
expect(contact.publicKeySet.encryption).to.equal('my_public_key')
expect(contact.publicKeySet.signing).to.equal('my_signing_public_key')
expect(contact.contactUuid).to.equal('123')
})
it('should create self contact on registration', async () => {
const selfContact = context.contacts.getSelfContact()
expect(selfContact).to.not.be.undefined
expect(selfContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(selfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
})
it('should create self contact on sign in if it does not exist', async () => {
let selfContact = context.contacts.getSelfContact()
await context.mutator.setItemToBeDeleted(selfContact)
await context.sync()
await context.signout()
await context.signIn()
selfContact = context.contacts.getSelfContact()
expect(selfContact).to.not.be.undefined
})
it('should update self contact on password change', async () => {
const selfContact = context.contacts.getSelfContact()
await context.changePassword('new_password')
const updatedSelfContact = context.contacts.getSelfContact()
expect(updatedSelfContact.publicKeySet.encryption).to.not.equal(selfContact.publicKeySet.encryption)
expect(updatedSelfContact.publicKeySet.signing).to.not.equal(selfContact.publicKeySet.signing)
expect(updatedSelfContact.publicKeySet.encryption).to.equal(context.publicKey)
expect(updatedSelfContact.publicKeySet.signing).to.equal(context.signingPublicKey)
})
it('should not be able to delete self contact', async () => {
const selfContact = context.contacts.getSelfContact()
await Factory.expectThrowsAsync(() => context.contacts.deleteContact(selfContact), 'Cannot delete self')
})
it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,204 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault crypto', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
})
describe('root key', () => {
it('root key loaded from disk should have keypairs', async () => {
const appIdentifier = context.identifier
await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined
})
it('changing user password should re-encrypt all key system root keys', async () => {
console.error('TODO: implement')
})
it('changing user password should re-encrypt all trusted contacts', async () => {
console.error('TODO: implement')
})
})
describe('persistent content signature', () => {
it('storage payloads should include signatureData', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
await context.sync()
const rawPayloads = await context.application.diskStorageService.getAllRawPayloads()
const noteRawPayload = rawPayloads.find((payload) => payload.uuid === note.uuid)
expect(noteRawPayload.signatureData).to.not.be.undefined
await deinitContactContext()
})
it('changing item content should erase existing signatureData', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
await context.changeNoteTitleAndSync(updatedNote, 'new title 2')
updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.signatureData).to.be.undefined
await deinitContactContext()
})
it('encrypting an item into storage then loading it should verify authenticity of original content rather than most recent symmetric signature', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(note, 'new title')
/** Override decrypt result to return failing signature */
const objectToSpy = context.encryption
sinon.stub(objectToSpy, 'decryptSplit').callsFake(async (split) => {
objectToSpy.decryptSplit.restore()
const decryptedPayloads = await objectToSpy.decryptSplit(split)
expect(decryptedPayloads.length).to.equal(1)
const payload = decryptedPayloads[0]
const mutatedPayload = new DecryptedPayload({
...payload.ejected(),
signatureData: {
...payload.signatureData,
result: {
...payload.signatureData.result,
passes: false,
},
},
})
return [mutatedPayload]
})
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.content.title).to.equal('new title')
expect(updatedNote.signatureData.result.passes).to.equal(false)
const appIdentifier = context.identifier
await context.deinit()
let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData.result.passes).to.equal(false)
/** Changing the content now should clear failing signature */
await recreatedContext.changeNoteTitleAndSync(updatedNote, 'new title 2')
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData).to.be.undefined
await recreatedContext.deinit()
recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
/** Decrypting from storage will now verify current user symmetric signature only */
updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.signatureData.result.passes).to.equal(true)
await recreatedContext.deinit()
await deinitContactContext()
})
})
describe('symmetrically encrypted items', () => {
it('created items with a payload source of remote saved should not have signature data', async () => {
const note = await context.createSyncedNote()
expect(note.payload.source).to.equal(PayloadSource.RemoteSaved)
expect(note.signatureData).to.be.undefined
})
it('retrieved items that are then remote saved should have their signature data cleared', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await contactContext.changeNoteTitleAndSync(contactContext.items.findItem(note.uuid), 'new title')
await context.sync()
expect(context.items.findItem(note.uuid).signatureData).to.not.be.undefined
await context.changeNoteTitleAndSync(context.items.findItem(note.uuid), 'new title')
expect(context.items.findItem(note.uuid).signatureData).to.be.undefined
await deinitContactContext()
})
it('should allow client verification of authenticity of shared item changes', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
expect(context.contacts.isItemAuthenticallySigned(note)).to.equal('not-applicable')
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal('yes')
await contactContext.changeNoteTitleAndSync(contactNote, 'new title')
await context.sync()
let updatedNote = context.items.findItem(note.uuid)
expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal('yes')
await deinitContactContext()
})
})
describe('keypair revocation', () => {
it('should be able to revoke non-current keypair', async () => {
console.error('TODO')
})
it('revoking a keypair should send a keypair revocation event to trusted contacts', async () => {
console.error('TODO')
})
it('should not be able to revoke current key pair', async () => {
console.error('TODO')
})
it('should distrust revoked keypair as contact', async () => {
console.error('TODO')
})
})
})

View File

@@ -0,0 +1,159 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault deletion', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should remove item from all user devices when item is deleted permanently', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid)
await context.mutator.setItemToBeDeleted(note)
await context.sync()
await contactContext.sync()
await promise
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const collaboratorNote = contactContext.items.findItem(note.uuid)
expect(collaboratorNote).to.be.undefined
await deinitContactContext()
})
it('attempting to delete a note received by and already deleted by another person should not cause infinite conflicts', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid)
await context.mutator.setItemToBeDeleted(note)
await contactContext.mutator.setItemToBeDeleted(note)
await context.sync()
await contactContext.sync()
await promise
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const collaboratorNote = contactContext.items.findItem(note.uuid)
expect(collaboratorNote).to.be.undefined
await deinitContactContext()
})
it('deleting a shared vault should remove all vault items from collaborator devices', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await sharedVaults.deleteSharedVault(sharedVault)
await contactContext.sync()
const originatorNote = context.items.findItem(note.uuid)
expect(originatorNote).to.be.undefined
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactNote).to.be.undefined
await deinitContactContext()
})
it('being removed from shared vault should remove shared vault items locally', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactNote).to.not.be.undefined
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.sync()
const updatedContactNote = contactContext.items.findItem(note.uuid)
expect(updatedContactNote).to.be.undefined
await deinitContactContext()
})
it('leaving a shared vault should remove its items locally', async () => {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Admin)
const originalNote = contactContext.items.findItem(note.uuid)
expect(originalNote).to.not.be.undefined
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
await contactContext.sharedVaults.leaveSharedVault(contactVault)
const updatedContactNote = contactContext.items.findItem(note.uuid)
expect(updatedContactNote).to.be.undefined
const vault = await contactContext.vaults.getVault({ keySystemIdentifier: contactVault.systemIdentifier })
expect(vault).to.be.undefined
await deinitContactContext()
})
it('removing an item from a vault should remove it from collaborator devices', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.vaults.removeItemFromVault(note)
await context.changeNoteTitleAndSync(note, 'new title')
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.not.equal('new title')
expect(receivedNote.title).to.equal(note.title)
await deinitContactContext()
})
it('should remove shared vault member', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const originalSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
expect(originalSharedVaultUsers.length).to.equal(2)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(isClientDisplayableError(result)).to.be.false
const updatedSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
expect(updatedSharedVaultUsers.length).to.equal(1)
await deinitContactContext()
})
it('being removed from a shared vault should delete respective vault listing', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,259 @@
import * as Factory from '../lib/factory.js'
import * as Files from '../lib/Files.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault files', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
await context.publicMockSubscriptionPurchaseEvent()
})
describe('private vaults', () => {
it('should be able to upload and download file to vault as owner', async () => {
const vault = await Collaboration.createPrivateVault(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, vault)
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
expect(file.key_system_identifier).to.equal(vault.systemIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
})
})
it('should be able to upload and download file to vault as owner', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
expect(file.key_system_identifier).to.equal(sharedVault.systemIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a user file to a vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const sharedVault = await Collaboration.createSharedVault(context)
const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, addedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a shared vault file to another shared vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const firstVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault)
const secondVault = await Collaboration.createSharedVault(context)
const movedFile = await vaults.moveItemToVault(secondVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, movedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a shared vault file to a non-shared vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const firstVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault)
const privateVault = await Collaboration.createPrivateVault(context)
const addedFile = await vaults.moveItemToVault(privateVault, uploadedFile)
const downloadedBytes = await Files.downloadFile(context.files, addedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('moving a note to a vault should also moved linked files', async () => {
const note = await context.createSyncedNote()
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const file = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const updatedFile = await context.application.mutator.associateFileWithNote(file, note)
const sharedVault = await Collaboration.createSharedVault(context)
vaults.alerts.confirmV2 = () => Promise.resolve(true)
await vaults.moveItemToVault(sharedVault, note)
const latestFile = context.items.findItem(updatedFile.uuid)
expect(vaults.getItemVault(latestFile).uuid).to.equal(sharedVault.uuid)
expect(vaults.getItemVault(context.items.findItem(note.uuid)).uuid).to.equal(sharedVault.uuid)
const downloadedBytes = await Files.downloadFile(context.files, latestFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to move a file out of its vault', async () => {
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const sharedVault = await Collaboration.createSharedVault(context)
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
const removedFile = await vaults.removeItemFromVault(uploadedFile)
expect(removedFile.key_system_identifier).to.not.be.ok
const downloadedBytes = await Files.downloadFile(context.files, removedFile)
expect(downloadedBytes).to.eql(buffer)
})
it('should be able to download vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const sharedFile = contactContext.items.findItem(uploadedFile.uuid)
expect(sharedFile).to.not.be.undefined
expect(sharedFile.remoteIdentifier).to.equal(uploadedFile.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should be able to upload vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(contactContext.files, buffer, 'my-file', 'md', 1000, sharedVault)
await context.sync()
const file = context.items.findItem(uploadedFile.uuid)
expect(file).to.not.be.undefined
expect(file.remoteIdentifier).to.equal(file.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(context.files, file)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should be able to delete vault file as write user', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Write)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const file = contactContext.items.findItem(uploadedFile.uuid)
const result = await contactContext.files.deleteFile(file)
expect(result).to.be.undefined
const foundFile = contactContext.items.findItem(file.uuid)
expect(foundFile).to.be.undefined
await deinitContactContext()
})
it('should not be able to delete vault file as read user', async () => {
context.anticipateConsoleError('Could not create valet token')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
const file = contactContext.items.findItem(uploadedFile.uuid)
const result = await contactContext.files.deleteFile(file)
expect(isClientDisplayableError(result)).to.be.true
const foundFile = contactContext.items.findItem(file.uuid)
expect(foundFile).to.not.be.undefined
await deinitContactContext()
})
it('should be able to download recently moved vault file as collaborator', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000)
const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile)
await contactContext.sync()
const sharedFile = contactContext.items.findItem(addedFile.uuid)
expect(sharedFile).to.not.be.undefined
expect(sharedFile.remoteIdentifier).to.equal(addedFile.remoteIdentifier)
const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile)
expect(downloadedBytes).to.eql(buffer)
await deinitContactContext()
})
it('should not be able to download file after being removed from vault', async () => {
context.anticipateConsoleError('Could not create valet token')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const response = await fetch('/mocha/assets/small_file.md')
const buffer = new Uint8Array(await response.arrayBuffer())
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const file = contactContext.items.findItem(uploadedFile.uuid)
await Factory.expectThrowsAsync(() => Files.downloadFile(contactContext.files, file), 'Could not download file')
await deinitContactContext()
})
})

View File

@@ -0,0 +1,229 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault invites', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should invite contact to vault', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
const vaultInvite = await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
expect(vaultInvite).to.not.be.undefined
expect(vaultInvite.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
expect(vaultInvite.user_uuid).to.equal(contact.contactUuid)
expect(vaultInvite.encrypted_message).to.not.be.undefined
expect(vaultInvite.permissions).to.equal(SharedVaultPermission.Write)
expect(vaultInvite.updated_at_timestamp).to.not.be.undefined
expect(vaultInvite.created_at_timestamp).to.not.be.undefined
await deinitContactContext()
})
it('invites from trusted contact should be pending as trusted', async () => {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.true
await deinitContactContext()
})
it('invites from untrusted contact should be pending as untrusted', async () => {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedAndUntrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.false
await deinitContactContext()
})
it('invite should include delegated trusted contacts', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
const invites = thirdPartyContext.sharedVaults.getCachedPendingInviteRecords()
const message = invites[0].message
const delegatedContacts = message.data.trustedContacts
expect(delegatedContacts.length).to.equal(1)
expect(delegatedContacts[0].contactUuid).to.equal(contactContext.userUuid)
await deinitThirdPartyContext()
await deinitContactContext()
})
it('should sync a shared vault from scratch after accepting an invitation', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
/** Create a mutually trusted contact */
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
/** Sync the contact context so that they wouldn't naturally receive changes made before this point */
await contactContext.sync()
await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
/** Contact should now sync and expect to find note */
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
await contactContext.sync()
await Collaboration.acceptAllInvites(contactContext)
await promise
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.equal('foo')
expect(receivedNote.text).to.equal(note.text)
await deinitContactContext()
})
it('received invites from untrusted contact should not be trusted', async () => {
await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await deinitContactContext()
})
it('received invites from contact who becomes trusted after receipt of invite should be trusted', async () => {
await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.true
await deinitContactContext()
})
it('received items should contain the uuid of the contact who sent the item', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.user_uuid).to.equal(context.userUuid)
await deinitContactContext()
})
it('items should contain the uuid of the last person who edited it', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote.last_edited_by_uuid).to.not.be.undefined
expect(receivedNote.last_edited_by_uuid).to.equal(context.userUuid)
await contactContext.changeNoteTitleAndSync(receivedNote, 'new title')
await context.sync()
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.last_edited_by_uuid).to.not.be.undefined
expect(updatedNote.last_edited_by_uuid).to.equal(contactContext.userUuid)
await deinitContactContext()
})
it('canceling an invite should remove it from recipient pending invites', async () => {
const { invite, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const preInvites = await contactContext.sharedVaults.downloadInboundInvites()
expect(preInvites.length).to.equal(1)
await sharedVaults.deleteInvite(invite)
const postInvites = await contactContext.sharedVaults.downloadInboundInvites()
expect(postInvites.length).to.equal(0)
await deinitContactContext()
})
it('when inviter keypair changes, recipient should still be able to trust and decrypt previous invite', async () => {
console.error('TODO: implement test')
})
it('should delete all inbound invites after changing user password', async () => {
/** Invites to user are encrypted with old keypair and are no longer decryptable */
console.error('TODO: implement test')
})
it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => {
const privateVault = await context.vaults.createUserInputtedPasswordVault({
name: 'My Private Vault',
userInputtedPassword: 'password',
storagePreference: KeySystemRootKeyStorageMode.Ephemeral,
})
const note = await context.createSyncedNote('foo', 'bar')
await context.vaults.moveItemToVault(privateVault, note)
const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault)
const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault(
context,
sharedVault,
)
await Collaboration.acceptAllInvites(thirdPartyContext)
const contextNote = thirdPartyContext.items.findItem(note.uuid)
expect(contextNote).to.not.be.undefined
expect(contextNote.title).to.equal('foo')
expect(contextNote.text).to.equal(note.text)
await deinitThirdPartyContext()
})
})

View File

@@ -0,0 +1,121 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault items', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should add item to shared vault with no other members', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const sharedVault = await Collaboration.createSharedVault(context)
await Collaboration.moveItemToVault(context, sharedVault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
expect(updatedNote.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
})
it('should add item to shared vault with contact', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const { sharedVault, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
await Collaboration.moveItemToVault(context, sharedVault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier)
await deinitContactContext()
})
it('received items from previously trusted contact should be decrypted', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
const sharedVault = await Collaboration.createSharedVault(context)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
contactContext.lockSyncing()
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await Collaboration.moveItemToVault(context, sharedVault, note)
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
contactContext.unlockSyncing()
await contactContext.sync()
await Collaboration.acceptAllInvites(contactContext)
await promise
const receivedItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(receivedItemsKey).to.not.be.undefined
expect(receivedItemsKey.itemsKey).to.not.be.undefined
const receivedNote = contactContext.items.findItem(note.uuid)
expect(receivedNote.title).to.equal('foo')
expect(receivedNote.text).to.equal(note.text)
await deinitContactContext()
})
it('shared vault creator should receive changes from other members', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
await contactContext.sync()
await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => {
mutator.title = 'new title'
})
await contactContext.sync()
await context.sync()
const receivedNote = context.items.findItem(note.uuid)
expect(receivedNote.title).to.equal('new title')
await deinitContactContext()
})
it('items added by collaborator should be received by shared vault owner', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const newNote = await contactContext.createSyncedNote('new note', 'new note text')
await Collaboration.moveItemToVault(contactContext, sharedVault, newNote)
await context.sync()
const receivedNote = context.items.findItem(newNote.uuid)
expect(receivedNote).to.not.be.undefined
expect(receivedNote.title).to.equal('new note')
await deinitContactContext()
})
it('adding item to vault while belonging to other vault should move the item to new vault', async () => {
console.error('TODO: implement test')
})
})

View File

@@ -0,0 +1,269 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault key rotation', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
sharedVaults = context.sharedVaults
})
it('should reencrypt all items keys belonging to key system', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const spy = sinon.spy(context.encryption, 'reencryptKeySystemItemsKeysForVault')
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
expect(spy.callCount).to.equal(1)
deinitContactContext()
})
it("rotating a vault's key should send an asymmetric message to all members", async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const outboundMessages = await context.asymmetric.getOutboundMessages()
const expectedMessages = ['root key change', 'vault metadata change']
expect(outboundMessages.length).to.equal(expectedMessages.length)
const message = outboundMessages[0]
expect(message).to.not.be.undefined
expect(message.user_uuid).to.equal(contactContext.userUuid)
expect(message.encrypted_message).to.not.be.undefined
await deinitContactContext()
})
it('should update recipient vault display listing with new key params', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
contactContext.unlockSyncing()
await contactContext.sync()
const vault = await contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(vault.rootKeyParams).to.eql(rootKey.keyParams)
await deinitContactContext()
})
it('should receive new key system items key', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(previousPrimaryItemsKey).to.not.be.undefined
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing()
await contactContext.sync()
await contactPromise
const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier)
expect(newPrimaryItemsKey).to.not.be.undefined
expect(newPrimaryItemsKey.uuid).to.not.equal(previousPrimaryItemsKey.uuid)
expect(newPrimaryItemsKey.itemsKey).to.not.eql(previousPrimaryItemsKey.itemsKey)
await deinitContactContext()
})
it("rotating a vault's key with a pending invite should create new invite and delete old", async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
contactContext.lockSyncing()
const originalOutboundInvites = await sharedVaults.getOutboundInvites()
expect(originalOutboundInvites.length).to.equal(1)
const originalInviteMessage = originalOutboundInvites[0].encrypted_message
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const updatedOutboundInvites = await sharedVaults.getOutboundInvites()
expect(updatedOutboundInvites.length).to.equal(1)
const joinInvite = updatedOutboundInvites[0]
expect(joinInvite.encrypted_message).to.not.be.undefined
expect(joinInvite.encrypted_message).to.not.equal(originalInviteMessage)
await deinitContactContext()
})
it('new key system items key in rotated shared vault should belong to shared vault', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
await vaults.rotateVaultRootKey(sharedVault)
const keySystemItemsKeys = context.keys
.getAllKeySystemItemsKeys()
.filter((key) => key.key_system_identifier === sharedVault.systemIdentifier)
expect(keySystemItemsKeys.length).to.equal(2)
for (const key of keySystemItemsKeys) {
expect(key.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid)
}
})
it('should update existing key-change messages instead of creating new ones', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await firstPromise
const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages()
const expectedMessages = ['root key change', 'vault metadata change']
expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length)
const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0]
const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await secondPromise
const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages()
expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length)
const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0]
expect(messageAfterSecondChange.encrypted_message).to.not.equal(messageAfterFirstChange.encrypted_message)
expect(messageAfterSecondChange.uuid).to.not.equal(messageAfterFirstChange.uuid)
await deinitContactContext()
})
it('key change messages should be automatically processed by trusted contacts', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.anticipateConsoleError(
'(2x) Error decrypting contentKey from parameters',
'Items keys are encrypted with new root key and are later decrypted in the test',
)
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await promise
const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
contactContext.unlockSyncing()
await contactContext.sync()
expect(acceptMessage.callCount).to.equal(1)
await deinitContactContext()
})
it('should rotate key system root key after removing vault member', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const originalKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const newKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
expect(newKeySystemRootKey.keyParams.creationTimestamp).to.be.greaterThan(
originalKeySystemRootKey.keyParams.creationTimestamp,
)
expect(newKeySystemRootKey.key).to.not.equal(originalKeySystemRootKey.key)
await deinitContactContext()
})
it('should throw if attempting to change password of locked vault', async () => {
console.error('TODO: implement')
})
it('should respect storage preference when rotating key system root key', async () => {
console.error('TODO: implement')
})
it('should change storage preference from synced to local', async () => {
console.error('TODO: implement')
})
it('should change storage preference from local to synced', async () => {
console.error('TODO: implement')
})
it('should resync key system items key if it is encrypted with noncurrent key system root key', async () => {
console.error('TODO: implement')
})
it('should change password type from user inputted to randomized', async () => {
console.error('TODO: implement')
})
it('should change password type from randomized to user inputted', async () => {
console.error('TODO: implement')
})
it('should not be able to change storage mode of third party vault', async () => {
console.error('TODO: implement')
})
})

View File

@@ -0,0 +1,133 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vault permissions', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('non-admin user should not be able to invite user', async () => {
context.anticipateConsoleError('Could not create invite')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const thirdParty = await Collaboration.createContactContext()
const thirdPartyContact = await Collaboration.createTrustedContactForUserOfContext(
contactContext,
thirdParty.contactContext,
)
const result = await contactContext.sharedVaults.inviteContactToSharedVault(
sharedVault,
thirdPartyContact,
SharedVaultPermission.Write,
)
expect(isClientDisplayableError(result)).to.be.true
await deinitContactContext()
})
it('should not be able to leave shared vault as creator', async () => {
context.anticipateConsoleError('Could not delete user')
const sharedVault = await Collaboration.createSharedVault(context)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, context.userUuid)
expect(isClientDisplayableError(result)).to.be.true
})
it('should be able to leave shared vault as added admin', async () => {
const { contactVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Admin)
const result = await contactContext.sharedVaults.leaveSharedVault(contactVault)
expect(isClientDisplayableError(result)).to.be.false
await deinitContactContext()
})
it('non-admin user should not be able to create or update vault items keys with the server', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const keySystemItemsKey = contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)[0]
await contactContext.mutator.changeItem(keySystemItemsKey, () => {})
const promise = contactContext.resolveWithConflicts()
await contactContext.sync()
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.KeySystemItemsKey)
await deinitContactContext()
})
it('read user should not be able to make changes to items', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read)
const note = await context.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(context, sharedVault, note)
await contactContext.sync()
await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => {
mutator.title = 'new title'
})
const promise = contactContext.resolveWithConflicts()
await contactContext.sync()
const conflicts = await promise
expect(conflicts.length).to.equal(1)
expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note)
await deinitContactContext()
})
it('should be able to move item from vault to user as a write user if the item belongs to me', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const note = await contactContext.createSyncedNote('foo', 'bar')
await Collaboration.moveItemToVault(contactContext, sharedVault, note)
await contactContext.sync()
const promise = contactContext.resolveWithConflicts()
await contactContext.vaults.removeItemFromVault(note)
const conflicts = await promise
expect(conflicts.length).to.equal(0)
const duplicateNote = contactContext.findDuplicateNote(note.uuid)
expect(duplicateNote).to.be.undefined
const existingNote = contactContext.items.findItem(note.uuid)
expect(existingNote.key_system_identifier).to.not.be.ok
await deinitContactContext()
})
})

View File

@@ -0,0 +1,98 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('public key cryptography', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sessions
let encryption
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sessions = context.application.sessions
encryption = context.encryption
})
it('should create keypair during registration', () => {
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
})
it('should populate keypair during sign in', async () => {
const email = context.email
const password = context.password
await context.signout()
const recreatedContext = await Factory.createAppContextWithRealCrypto()
await recreatedContext.launch()
recreatedContext.email = email
recreatedContext.password = password
await recreatedContext.signIn()
expect(recreatedContext.sessions.getPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getKeyPair().privateKey).to.not.be.undefined
expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined
expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined
})
it('should rotate keypair during password change', async () => {
const oldPublicKey = sessions.getPublicKey()
const oldPrivateKey = encryption.getKeyPair().privateKey
const oldSigningPublicKey = sessions.getSigningPublicKey()
const oldSigningPrivateKey = encryption.getSigningKeyPair().privateKey
await context.changePassword('new_password')
expect(sessions.getPublicKey()).to.not.be.undefined
expect(encryption.getKeyPair().privateKey).to.not.be.undefined
expect(sessions.getPublicKey()).to.not.equal(oldPublicKey)
expect(encryption.getKeyPair().privateKey).to.not.equal(oldPrivateKey)
expect(sessions.getSigningPublicKey()).to.not.be.undefined
expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined
expect(sessions.getSigningPublicKey()).to.not.equal(oldSigningPublicKey)
expect(encryption.getSigningKeyPair().privateKey).to.not.equal(oldSigningPrivateKey)
})
it('should allow option to enable collaboration for previously signed in accounts', async () => {
const newContext = await Factory.createAppContextWithRealCrypto()
await newContext.launch()
await newContext.register()
const rootKey = await newContext.encryption.getRootKey()
const mutatedRootKey = CreateNewRootKey({
...rootKey.content,
encryptionKeyPair: undefined,
signingKeyPair: undefined,
})
await newContext.encryption.setRootKey(mutatedRootKey)
expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.true
const result = await newContext.application.user.updateAccountWithFirstTimeKeyPair()
expect(result.error).to.be.undefined
expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false
})
})

View File

@@ -0,0 +1,114 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('shared vaults', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
vaults = context.vaults
})
it('should update vault name and description', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
await vaults.changeVaultNameAndDescription(sharedVault, {
name: 'new vault name',
description: 'new vault description',
})
const updatedVault = vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.equal('new vault name')
expect(updatedVault.description).to.equal('new vault description')
const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
await contactContext.sync()
await promise
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(contactVault.name).to.equal('new vault name')
expect(contactVault.description).to.equal('new vault description')
await deinitContactContext()
})
it('being removed from a shared vault should remove the vault', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(result).to.be.undefined
const promise = contactContext.resolveWhenUserMessagesProcessingCompletes()
await contactContext.sync()
await promise
expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier)
await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
await deinitContactContext()
await recreatedContext.deinit()
})
it('deleting a shared vault should remove vault from contact context', async () => {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.sharedVaults.deleteSharedVault(sharedVault)
expect(result).to.be.undefined
const promise = contactContext.resolveWhenUserMessagesProcessingCompletes()
await contactContext.sync()
await promise
expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier)
await recreatedContext.launch()
expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined
expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined
expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty
await deinitContactContext()
await recreatedContext.deinit()
})
it('should convert a vault to a shared vault', async () => {
console.error('TODO')
})
it('should send metadata change message when changing name or description', async () => {
console.error('TODO')
})
})

View File

@@ -0,0 +1,250 @@
import * as Factory from '../lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('vaults', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
afterEach(async function () {
await context.deinit()
localStorage.clear()
})
beforeEach(async function () {
localStorage.clear()
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
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({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault.systemIdentifier).to.not.be.undefined
expect(typeof vault.systemIdentifier).to.equal('string')
const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier)
expect(keySystemItemsKey).to.not.be.undefined
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined
expect(keySystemItemsKey.keyVersion).to.not.be.undefined
})
it('should be able to create an offline vault with app passcode', async () => {
await context.application.addPasscode('123')
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault.systemIdentifier).to.not.be.undefined
expect(typeof vault.systemIdentifier).to.equal('string')
const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier)
expect(keySystemItemsKey).to.not.be.undefined
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined
expect(keySystemItemsKey.keyVersion).to.not.be.undefined
})
it('should add item to offline vault', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const item = await context.createSyncedNote()
await vaults.moveItemToVault(vault, item)
const updatedItem = context.items.findItem(item.uuid)
expect(updatedItem.key_system_identifier).to.equal(vault.systemIdentifier)
})
it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
describe('porting from offline to online', () => {
it('should maintain vault system identifiers across items after registration', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.register()
await context.sync()
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const notes = recreatedContext.notes
expect(notes.length).to.equal(1)
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier)
await recreatedContext.deinit()
})
it('should decrypt vault items', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.register()
await context.sync()
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
})
})
describe('online', () => {
beforeEach(async () => {
await context.register()
})
it('should create a vault', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
expect(vault).to.not.be.undefined
const keySystemItemsKeys = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)
expect(keySystemItemsKeys.length).to.equal(1)
const keySystemItemsKey = keySystemItemsKeys[0]
expect(keySystemItemsKey instanceof KeySystemItemsKey).to.be.true
expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier)
})
it('should add item to vault', async () => {
const note = await context.createSyncedNote('foo', 'bar')
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
await vaults.moveItemToVault(vault, note)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier)
})
describe('client timing', () => {
it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => {
const appIdentifier = context.identifier
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await context.deinit()
const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier)
await recreatedContext.launch()
const updatedNote = recreatedContext.items.findItem(note.uuid)
expect(updatedNote.title).to.equal('foo')
expect(updatedNote.text).to.equal('bar')
await recreatedContext.deinit()
})
})
describe('key system root key rotation', () => {
it('rotating a key system root key should create a new vault items key', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const keySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0]
await vaults.rotateVaultRootKey(vault)
const updatedKeySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0]
expect(updatedKeySystemItemsKey).to.not.be.undefined
expect(updatedKeySystemItemsKey.uuid).to.not.equal(keySystemItemsKey.uuid)
})
it('deleting a vault should delete all its items', async () => {
const vault = await vaults.createRandomizedVault({
name: 'My Vault',
storagePreference: KeySystemRootKeyStorageMode.Synced,
})
const note = await context.createSyncedNote('foo', 'bar')
await vaults.moveItemToVault(vault, note)
await vaults.deleteVault(vault)
const updatedNote = context.items.findItem(note.uuid)
expect(updatedNote).to.be.undefined
})
})
})
})