internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
277
packages/snjs/mocha/vaults/asymmetric-messages.test.js
Normal file
277
packages/snjs/mocha/vaults/asymmetric-messages.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
186
packages/snjs/mocha/vaults/conflicts.test.js
Normal file
186
packages/snjs/mocha/vaults/conflicts.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
83
packages/snjs/mocha/vaults/contacts.test.js
Normal file
83
packages/snjs/mocha/vaults/contacts.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
204
packages/snjs/mocha/vaults/crypto.test.js
Normal file
204
packages/snjs/mocha/vaults/crypto.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
159
packages/snjs/mocha/vaults/deletion.test.js
Normal file
159
packages/snjs/mocha/vaults/deletion.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
259
packages/snjs/mocha/vaults/files.test.js
Normal file
259
packages/snjs/mocha/vaults/files.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
229
packages/snjs/mocha/vaults/invites.test.js
Normal file
229
packages/snjs/mocha/vaults/invites.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
121
packages/snjs/mocha/vaults/items.test.js
Normal file
121
packages/snjs/mocha/vaults/items.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
269
packages/snjs/mocha/vaults/key_rotation.test.js
Normal file
269
packages/snjs/mocha/vaults/key_rotation.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
133
packages/snjs/mocha/vaults/permissions.test.js
Normal file
133
packages/snjs/mocha/vaults/permissions.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
98
packages/snjs/mocha/vaults/pkc.test.js
Normal file
98
packages/snjs/mocha/vaults/pkc.test.js
Normal 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
|
||||
})
|
||||
})
|
||||
114
packages/snjs/mocha/vaults/shared_vaults.test.js
Normal file
114
packages/snjs/mocha/vaults/shared_vaults.test.js
Normal 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')
|
||||
})
|
||||
})
|
||||
250
packages/snjs/mocha/vaults/vaults.test.js
Normal file
250
packages/snjs/mocha/vaults/vaults.test.js
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user