684 lines
24 KiB
JavaScript
684 lines
24 KiB
JavaScript
/* eslint-disable no-unused-expressions */
|
|
/* eslint-disable no-undef */
|
|
import * as Factory from './lib/factory.js'
|
|
chai.use(chaiAsPromised)
|
|
const expect = chai.expect
|
|
|
|
describe('key recovery service', function () {
|
|
this.timeout(Factory.TwentySecondTimeout)
|
|
|
|
const syncOptions = {
|
|
checkIntegrity: true,
|
|
awaitAll: true,
|
|
}
|
|
|
|
beforeEach(function () {
|
|
localStorage.clear()
|
|
})
|
|
|
|
afterEach(function () {
|
|
localStorage.clear()
|
|
})
|
|
|
|
it('when encountering an undecryptable items key, should recover through recovery wizard', async function () {
|
|
const namespace = Factory.randomString()
|
|
const context = await Factory.createAppContextWithFakeCrypto(namespace)
|
|
const unassociatedPassword = 'randfoo'
|
|
const unassociatedIdentifier = 'foorand'
|
|
|
|
const application = context.application
|
|
await context.launch({
|
|
receiveChallenge: (challenge) => {
|
|
application.submitValuesForChallenge(challenge, [
|
|
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
|
|
])
|
|
},
|
|
})
|
|
|
|
await context.register()
|
|
|
|
const randomRootKey = await application.protocolService.createRootKey(
|
|
unassociatedIdentifier,
|
|
unassociatedPassword,
|
|
KeyParamsOrigination.Registration,
|
|
)
|
|
const randomItemsKey = await application.protocolService.operatorManager.defaultOperator().createItemsKey()
|
|
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKey: {
|
|
items: [randomItemsKey.payload],
|
|
key: randomRootKey,
|
|
},
|
|
})
|
|
|
|
const errored = await application.protocolService.decryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [encrypted],
|
|
},
|
|
})
|
|
|
|
expect(errored.errorDecrypting).to.equal(true)
|
|
|
|
await application.payloadManager.emitPayload(errored, PayloadEmitSource.LocalInserted)
|
|
|
|
await context.resolveWhenKeyRecovered(errored.uuid)
|
|
|
|
expect(application.items.findItem(errored.uuid).errorDecrypting).to.not.be.ok
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
await context.deinit()
|
|
})
|
|
|
|
it('recovered keys with key params not matching servers should be synced if local root key does matches server', async function () {
|
|
/**
|
|
* This helps ensure server always has the most valid state,
|
|
* in case the recovery is being initiated from a server value in the first place
|
|
*/
|
|
const context = await Factory.createAppContextWithFakeCrypto()
|
|
const unassociatedPassword = 'randfoo'
|
|
const unassociatedIdentifier = 'foorand'
|
|
|
|
const application = context.application
|
|
|
|
await context.launch({
|
|
receiveChallenge: (challenge) => {
|
|
application.submitValuesForChallenge(challenge, [
|
|
CreateChallengeValue(challenge.prompts[0], unassociatedPassword),
|
|
])
|
|
},
|
|
})
|
|
await context.register()
|
|
|
|
const randomRootKey = await application.protocolService.createRootKey(
|
|
unassociatedIdentifier,
|
|
unassociatedPassword,
|
|
KeyParamsOrigination.Registration,
|
|
)
|
|
|
|
const randomItemsKey = await application.protocolService.operatorManager.defaultOperator().createItemsKey()
|
|
|
|
await application.payloadManager.emitPayload(
|
|
randomItemsKey.payload.copy({ dirty: true, dirtyIndex: getIncrementedDirtyIndex() }),
|
|
PayloadEmitSource.LocalInserted,
|
|
)
|
|
|
|
await context.sync()
|
|
|
|
const originalSyncTime = application.payloadManager.findOne(randomItemsKey.uuid).lastSyncEnd.getTime()
|
|
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKey: {
|
|
items: [randomItemsKey.payload],
|
|
key: randomRootKey,
|
|
},
|
|
})
|
|
|
|
const errored = await application.protocolService.decryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [encrypted],
|
|
},
|
|
})
|
|
|
|
await application.payloadManager.emitPayload(errored, PayloadEmitSource.LocalInserted)
|
|
|
|
const recoveryPromise = context.resolveWhenKeyRecovered(errored.uuid)
|
|
|
|
await context.sync()
|
|
|
|
await recoveryPromise
|
|
|
|
expect(application.payloadManager.findOne(errored.uuid).lastSyncEnd.getTime()).to.be.above(originalSyncTime)
|
|
|
|
await context.deinit()
|
|
})
|
|
|
|
it('recovered keys with key params not matching servers should not be synced if local root key does not match server', async function () {
|
|
/**
|
|
* Assume Application A has been through these states:
|
|
* 1. Registration + Items Key A + Root Key A
|
|
* 2. Password change + Items Key B + Root Key B
|
|
* 3. Password change + Items Key C + Root Key C + Failure to correctly re-encrypt Items Key A and B with Root Key C
|
|
*
|
|
* Application B is not correctly in sync, and is only at State 1 (Registration + Items Key A)
|
|
*
|
|
* Application B receives Items Key B of Root Key B but for whatever reason ignores Items Key C of Root Key C.
|
|
*
|
|
* When it recovers Items Key B, it should not re-upload it to the server, because Application B's Root Key is not
|
|
* the current account's root key.
|
|
*/
|
|
|
|
const contextA = await Factory.createAppContextWithFakeCrypto()
|
|
await contextA.launch()
|
|
await contextA.register()
|
|
contextA.preventKeyRecoveryOfKeys()
|
|
|
|
const contextB = await Factory.createAppContextWithFakeCrypto('app-b', contextA.email, contextA.password)
|
|
await contextB.launch()
|
|
await contextB.signIn()
|
|
|
|
await contextA.changePassword('new-password-1')
|
|
const itemsKeyARootKeyB = contextA.itemsKeys[0]
|
|
const itemsKeyBRootKeyB = contextA.itemsKeys[1]
|
|
|
|
contextA.disableSyncingOfItems([itemsKeyARootKeyB.uuid, itemsKeyBRootKeyB.uuid])
|
|
await contextA.changePassword('new-password-2')
|
|
const itemsKeyCRootKeyC = contextA.itemsKeys[2]
|
|
|
|
contextB.disableKeyRecoveryServerSignIn()
|
|
contextB.preventKeyRecoveryOfKeys([itemsKeyCRootKeyC.uuid])
|
|
contextB.respondToAccountPasswordChallengeWith('new-password-1')
|
|
|
|
const recoveryPromise = Promise.all([
|
|
contextB.resolveWhenKeyRecovered(itemsKeyARootKeyB.uuid),
|
|
contextB.resolveWhenKeyRecovered(itemsKeyBRootKeyB.uuid),
|
|
])
|
|
|
|
const observedDirtyItemUuids = []
|
|
contextB.spyOnChangedItems((changed) => {
|
|
const dirty = changed.filter((i) => i.dirty)
|
|
extendArray(observedDirtyItemUuids, Uuids(dirty))
|
|
})
|
|
|
|
await contextB.sync()
|
|
await recoveryPromise
|
|
|
|
expect(observedDirtyItemUuids.includes(itemsKeyARootKeyB.uuid)).to.equal(false)
|
|
expect(observedDirtyItemUuids.includes(itemsKeyBRootKeyB.uuid)).to.equal(false)
|
|
|
|
await contextA.deinit()
|
|
await contextB.deinit()
|
|
})
|
|
|
|
it('when encountering many undecryptable items key with same key params, should only prompt once', async function () {
|
|
const namespace = Factory.randomString()
|
|
const unassociatedPassword = 'randfoo'
|
|
const unassociatedIdentifier = 'foorand'
|
|
let totalPromptCount = 0
|
|
|
|
const context = await Factory.createAppContextWithFakeCrypto(namespace)
|
|
const application = context.application
|
|
const receiveChallenge = (challenge) => {
|
|
totalPromptCount++
|
|
/** Give unassociated password when prompted */
|
|
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
|
|
}
|
|
await application.prepareForLaunch({ receiveChallenge })
|
|
await application.launch(true)
|
|
|
|
await Factory.registerUserToApplication({
|
|
application: application,
|
|
email: context.email,
|
|
password: context.password,
|
|
})
|
|
|
|
/** Create items key associated with a random root key */
|
|
const randomRootKey = await application.protocolService.createRootKey(
|
|
unassociatedIdentifier,
|
|
unassociatedPassword,
|
|
KeyParamsOrigination.Registration,
|
|
)
|
|
const randomItemsKey = await application.protocolService.operatorManager.defaultOperator().createItemsKey()
|
|
const randomItemsKey2 = await application.protocolService.operatorManager.defaultOperator().createItemsKey()
|
|
|
|
const encrypted = await application.protocolService.encryptSplit({
|
|
usesRootKey: {
|
|
items: [randomItemsKey.payload, randomItemsKey2.payload],
|
|
key: randomRootKey,
|
|
},
|
|
})
|
|
|
|
/** Attempt decryption and insert into rotation in errored state */
|
|
const decrypted = await application.protocolService.decryptSplit({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: encrypted,
|
|
},
|
|
})
|
|
|
|
await application.payloadManager.emitPayloads(decrypted, PayloadEmitSource.LocalInserted)
|
|
|
|
/** Wait and allow recovery wizard to complete */
|
|
await Factory.sleep(1.5)
|
|
|
|
/** Should be decrypted now */
|
|
expect(application.items.findItem(randomItemsKey.uuid).errorDecrypting).not.be.ok
|
|
expect(application.items.findItem(randomItemsKey2.uuid).errorDecrypting).not.be.ok
|
|
|
|
expect(totalPromptCount).to.equal(1)
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
await context.deinit()
|
|
})
|
|
|
|
it('when changing password on client B, client A should perform recovery flow', async function () {
|
|
const contextA = await Factory.createAppContextWithFakeCrypto()
|
|
await contextA.launch()
|
|
await contextA.register()
|
|
const originalItemsKey = contextA.application.items.getDisplayableItemsKeys()[0]
|
|
|
|
const contextB = await Factory.createAppContextWithFakeCrypto(
|
|
'another-namespace',
|
|
contextA.email,
|
|
contextA.password,
|
|
)
|
|
|
|
contextB.ignoreChallenges()
|
|
await contextB.launch()
|
|
await contextB.signIn()
|
|
|
|
const newPassword = `${Math.random()}`
|
|
|
|
const result = await contextB.application.changePassword(contextA.password, newPassword)
|
|
|
|
expect(result.error).to.not.be.ok
|
|
expect(contextB.application.items.getAnyItems(ContentType.ItemsKey).length).to.equal(2)
|
|
|
|
const newItemsKey = contextB.application.items.getDisplayableItemsKeys().find((k) => k.uuid !== originalItemsKey.uuid)
|
|
|
|
const note = await Factory.createSyncedNote(contextB.application)
|
|
|
|
const recoveryPromise = contextA.resolveWhenKeyRecovered(newItemsKey.uuid)
|
|
|
|
contextA.password = newPassword
|
|
|
|
await contextA.sync(syncOptions)
|
|
await recoveryPromise
|
|
|
|
/** Same previously errored key should now no longer be errored, */
|
|
expect(contextA.application.items.getAnyItems(ContentType.ItemsKey).length).to.equal(2)
|
|
for (const key of contextA.application.itemManager.getDisplayableItemsKeys()) {
|
|
expect(key.errorDecrypting).to.not.be.ok
|
|
}
|
|
|
|
const aKey = await contextA.application.protocolService.getRootKey()
|
|
const bKey = await contextB.application.protocolService.getRootKey()
|
|
expect(aKey.compare(bKey)).to.equal(true)
|
|
|
|
expect(contextA.application.items.findItem(note.uuid).errorDecrypting).to.not.be.ok
|
|
expect(contextB.application.items.findItem(note.uuid).errorDecrypting).to.not.be.ok
|
|
|
|
expect(contextA.application.syncService.isOutOfSync()).to.equal(false)
|
|
expect(contextB.application.syncService.isOutOfSync()).to.equal(false)
|
|
|
|
await contextA.deinit()
|
|
await contextB.deinit()
|
|
}).timeout(80000)
|
|
|
|
it('when items key associated with item is errored, item should be marked waiting for key', async function () {
|
|
const namespace = Factory.randomString()
|
|
const newPassword = `${Math.random()}`
|
|
const contextA = await Factory.createAppContextWithFakeCrypto(namespace)
|
|
const appA = contextA.application
|
|
await appA.prepareForLaunch({ receiveChallenge: () => {} })
|
|
await appA.launch(true)
|
|
|
|
await Factory.registerUserToApplication({
|
|
application: appA,
|
|
email: contextA.email,
|
|
password: contextA.password,
|
|
})
|
|
|
|
expect(appA.items.getItems(ContentType.ItemsKey).length).to.equal(1)
|
|
|
|
/** Create simultaneous appB signed into same account */
|
|
const appB = await Factory.createApplicationWithFakeCrypto('another-namespace')
|
|
await appB.prepareForLaunch({ receiveChallenge: () => {} })
|
|
await appB.launch(true)
|
|
|
|
await Factory.loginToApplication({
|
|
application: appB,
|
|
email: contextA.email,
|
|
password: contextA.password,
|
|
})
|
|
|
|
/** Change password on appB */
|
|
await appB.changePassword(contextA.password, newPassword)
|
|
const note = await Factory.createSyncedNote(appB)
|
|
await appB.sync.sync()
|
|
|
|
/** We expect the item in appA to be errored at this point, but we do not want it to recover */
|
|
await appA.sync.sync()
|
|
expect(appA.payloadManager.findOne(note.uuid).waitingForKey).to.equal(true)
|
|
|
|
console.warn('Expecting exceptions below as we destroy app during key recovery')
|
|
await Factory.safeDeinit(appA)
|
|
await Factory.safeDeinit(appB)
|
|
|
|
const recreatedAppA = await Factory.createApplicationWithFakeCrypto(namespace)
|
|
await recreatedAppA.prepareForLaunch({ receiveChallenge: () => {} })
|
|
await recreatedAppA.launch(true)
|
|
|
|
expect(recreatedAppA.payloadManager.findOne(note.uuid).errorDecrypting).to.equal(true)
|
|
expect(recreatedAppA.payloadManager.findOne(note.uuid).waitingForKey).to.equal(true)
|
|
await Factory.safeDeinit(recreatedAppA)
|
|
})
|
|
|
|
it('when client key params differ from server, and no matching items key exists to compare against, should perform sign in flow', async function () {
|
|
/**
|
|
* When a user changes password/email on client A, client B must update their root key to the new one.
|
|
* To do this, we can potentially avoid making a new sign in request (and creating a new session) by instead
|
|
* reading one of the undecryptable items key (which is potentially the new one that client A created). If the keyParams
|
|
* of that items key matches the servers, it means we can use those key params to compute our new local root key,
|
|
* instead of having to sign in.
|
|
*/
|
|
const unassociatedPassword = 'randfoo'
|
|
const context = await Factory.createAppContextWithFakeCrypto('some-namespace')
|
|
const application = context.application
|
|
|
|
const receiveChallenge = (challenge) => {
|
|
const isKeyRecoveryPrompt = challenge.subheading?.includes(KeyRecoveryStrings.KeyRecoveryPasswordRequired)
|
|
application.submitValuesForChallenge(challenge, [
|
|
CreateChallengeValue(challenge.prompts[0], isKeyRecoveryPrompt ? unassociatedPassword : context.password),
|
|
])
|
|
}
|
|
|
|
await application.prepareForLaunch({ receiveChallenge })
|
|
await application.launch(true)
|
|
await context.register()
|
|
|
|
const correctRootKey = await application.protocolService.getRootKey()
|
|
|
|
/**
|
|
* 1. Change our root key locally so that its keys params doesn't match the server's
|
|
* 2. Create an items key payload that is set to errorDecrypting, and which is encrypted
|
|
* with the incorrect root key, so that it cannot be used to validate the user's password
|
|
*/
|
|
|
|
const unassociatedIdentifier = 'foorand'
|
|
|
|
/** Create items key associated with a random root key */
|
|
const randomRootKey = await application.protocolService.createRootKey(
|
|
unassociatedIdentifier,
|
|
unassociatedPassword,
|
|
KeyParamsOrigination.Registration,
|
|
)
|
|
|
|
const signInFunction = sinon.spy(application.keyRecoveryService, 'performServerSignIn')
|
|
|
|
await application.protocolService.setRootKey(randomRootKey)
|
|
|
|
const correctItemsKey = await application.protocolService.operatorManager.defaultOperator().createItemsKey()
|
|
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKey: {
|
|
items: [correctItemsKey.payload],
|
|
key: randomRootKey,
|
|
},
|
|
})
|
|
|
|
const resolvePromise = Promise.all([
|
|
context.awaitSignInEvent(),
|
|
context.resolveWhenKeyRecovered(correctItemsKey.uuid),
|
|
])
|
|
|
|
await application.payloadManager.emitPayload(
|
|
encrypted.copy({
|
|
errorDecrypting: true,
|
|
dirty: true,
|
|
}),
|
|
PayloadEmitSource.LocalInserted,
|
|
)
|
|
|
|
await context.sync()
|
|
|
|
await resolvePromise
|
|
|
|
expect(signInFunction.callCount).to.equal(1)
|
|
|
|
const clientRootKey = await application.protocolService.getRootKey()
|
|
expect(clientRootKey.compare(correctRootKey)).to.equal(true)
|
|
|
|
const decryptedKey = application.items.findItem(correctItemsKey.uuid)
|
|
expect(decryptedKey).to.be.ok
|
|
expect(decryptedKey.content.itemsKey).to.equal(correctItemsKey.content.itemsKey)
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
await context.deinit()
|
|
})
|
|
|
|
it(`when encountering an items key that cannot be decrypted for which we already have a decrypted value,
|
|
it should be emitted as ignored`, async function () {
|
|
const context = await Factory.createAppContextWithFakeCrypto()
|
|
const application = context.application
|
|
await context.launch()
|
|
await context.register()
|
|
|
|
/** Create and emit errored encrypted items key payload */
|
|
const itemsKey = await application.protocolService.getSureDefaultItemsKey()
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [itemsKey.payload],
|
|
},
|
|
})
|
|
|
|
const newUpdated = new Date()
|
|
const errored = encrypted.copy({
|
|
content: '004:...',
|
|
errorDecrypting: true,
|
|
updated_at: newUpdated,
|
|
})
|
|
|
|
await context.receiveServerResponse({ retrievedItems: [errored.ejected()] })
|
|
|
|
/** Our current items key should not be overwritten */
|
|
const currentItemsKey = application.items.findItem(itemsKey.uuid)
|
|
expect(currentItemsKey.errorDecrypting).to.not.be.ok
|
|
expect(currentItemsKey.itemsKey).to.equal(itemsKey.itemsKey)
|
|
|
|
/** The timestamp of our current key should be updated however so we do not enter out of sync state */
|
|
expect(currentItemsKey.serverUpdatedAt.getTime()).to.equal(newUpdated.getTime())
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
|
|
await context.deinit()
|
|
})
|
|
|
|
it(`ignored key payloads should be added to undecryptables and recovered`, async function () {
|
|
const context = await Factory.createAppContextWithFakeCrypto()
|
|
const application = context.application
|
|
await context.launch()
|
|
await context.register()
|
|
|
|
const itemsKey = await application.protocolService.getSureDefaultItemsKey()
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [itemsKey.payload],
|
|
},
|
|
})
|
|
|
|
const newUpdated = new Date()
|
|
const errored = encrypted.copy({
|
|
errorDecrypting: true,
|
|
updated_at: newUpdated,
|
|
})
|
|
|
|
await application.payloadManager.emitDeltaEmit({
|
|
emits: [],
|
|
ignored: [errored],
|
|
source: PayloadEmitSource.RemoteRetrieved,
|
|
})
|
|
|
|
await context.resolveWhenKeyRecovered(itemsKey.uuid)
|
|
|
|
const latestItemsKey = application.items.findItem(itemsKey.uuid)
|
|
|
|
expect(latestItemsKey.errorDecrypting).to.not.be.ok
|
|
expect(latestItemsKey.itemsKey).to.equal(itemsKey.itemsKey)
|
|
expect(latestItemsKey.serverUpdatedAt.getTime()).to.equal(newUpdated.getTime())
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
|
|
await context.deinit()
|
|
})
|
|
|
|
it('application should prompt to recover undecryptables on launch', async function () {
|
|
const namespace = Factory.randomString()
|
|
const context = await Factory.createAppContextWithFakeCrypto(namespace)
|
|
const application = context.application
|
|
await context.launch()
|
|
await context.register()
|
|
|
|
/** Create and emit errored encrypted items key payload */
|
|
const itemsKey = await application.protocolService.getSureDefaultItemsKey()
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [itemsKey.payload],
|
|
},
|
|
})
|
|
|
|
context.disableKeyRecovery()
|
|
|
|
await application.payloadManager.emitDeltaEmit({
|
|
emits: [],
|
|
ignored: [
|
|
encrypted.copy({
|
|
errorDecrypting: true,
|
|
}),
|
|
],
|
|
source: PayloadEmitSource.RemoteRetrieved,
|
|
})
|
|
|
|
/** Allow enough time to persist to disk, but not enough to complete recovery wizard */
|
|
console.warn('Expecting some error below because we are destroying app in the middle of processing.')
|
|
|
|
await Factory.sleep(0.1)
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
|
|
await context.deinit()
|
|
|
|
const recreatedContext = await Factory.createAppContextWithFakeCrypto(namespace, context.email, context.password)
|
|
|
|
const recreatedApp = recreatedContext.application
|
|
|
|
const promise = recreatedContext.resolveWhenKeyRecovered(itemsKey.uuid)
|
|
|
|
await recreatedContext.launch()
|
|
|
|
await promise
|
|
|
|
await Factory.safeDeinit(recreatedApp)
|
|
})
|
|
|
|
it('when encountering an undecryptable 003 items key, should recover through recovery wizard', async function () {
|
|
const namespace = Factory.randomString()
|
|
const unassociatedPassword = 'randfoo'
|
|
const unassociatedIdentifier = 'foorand'
|
|
|
|
const context = await Factory.createAppContextWithFakeCrypto(namespace)
|
|
const application = context.application
|
|
const receiveChallenge = (challenge) => {
|
|
/** Give unassociated password when prompted */
|
|
application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)])
|
|
}
|
|
await application.prepareForLaunch({ receiveChallenge })
|
|
await application.launch(true)
|
|
|
|
await Factory.registerOldUser({
|
|
application: application,
|
|
email: context.email,
|
|
password: context.password,
|
|
version: ProtocolVersion.V003,
|
|
})
|
|
|
|
/** Create items key associated with a random root key */
|
|
const randomRootKey = await application.protocolService.createRootKey(
|
|
unassociatedIdentifier,
|
|
unassociatedPassword,
|
|
KeyParamsOrigination.Registration,
|
|
ProtocolVersion.V003,
|
|
)
|
|
const randomItemsKey = await application.protocolService.operatorManager
|
|
.operatorForVersion(ProtocolVersion.V003)
|
|
.createItemsKey()
|
|
|
|
const encrypted = await application.protocolService.encryptSplitSingle({
|
|
usesRootKey: {
|
|
items: [randomItemsKey.payload],
|
|
key: randomRootKey,
|
|
},
|
|
})
|
|
|
|
/** Attempt decryption and insert into rotation in errored state */
|
|
const decrypted = await application.protocolService.decryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [encrypted],
|
|
},
|
|
})
|
|
/** Expect to be errored */
|
|
expect(decrypted.errorDecrypting).to.equal(true)
|
|
|
|
/** Insert into rotation */
|
|
await application.payloadManager.emitPayload(decrypted, PayloadEmitSource.LocalInserted)
|
|
|
|
/** Wait and allow recovery wizard to complete */
|
|
await Factory.sleep(0.3)
|
|
|
|
/** Should be decrypted now */
|
|
expect(application.items.findItem(encrypted.uuid).errorDecrypting).to.not.be.ok
|
|
|
|
expect(application.syncService.isOutOfSync()).to.equal(false)
|
|
await context.deinit()
|
|
})
|
|
|
|
it('when replacing root key, new root key should be set before items key are re-saved to disk', async function () {
|
|
const contextA = await Factory.createAppContextWithFakeCrypto()
|
|
await contextA.launch()
|
|
await contextA.register()
|
|
|
|
const newPassword = 'new-password'
|
|
|
|
/** Create simultaneous appB signed into same account */
|
|
const contextB = await Factory.createAppContextWithFakeCrypto(
|
|
'another-namespace',
|
|
contextA.email,
|
|
contextA.password,
|
|
)
|
|
|
|
contextB.ignoreChallenges()
|
|
await contextB.launch()
|
|
await contextB.signIn()
|
|
const appB = contextB.application
|
|
|
|
/** Change password on appB */
|
|
const result = await appB.changePassword(contextA.password, newPassword)
|
|
expect(result.error).to.not.be.ok
|
|
contextA.password = newPassword
|
|
await appB.sync.sync()
|
|
|
|
const newDefaultKey = appB.protocolService.getSureDefaultItemsKey()
|
|
|
|
const encrypted = await appB.protocolService.encryptSplitSingle({
|
|
usesRootKeyWithKeyLookup: {
|
|
items: [newDefaultKey.payload],
|
|
},
|
|
})
|
|
|
|
/** Insert foreign items key into appA, which shouldn't be able to decrypt it yet */
|
|
const appA = contextA.application
|
|
await appA.payloadManager.emitPayload(
|
|
encrypted.copy({
|
|
errorDecrypting: true,
|
|
}),
|
|
PayloadEmitSource.LocalInserted,
|
|
)
|
|
|
|
await Factory.awaitFunctionInvokation(appA.keyRecoveryService, 'handleDecryptionOfAllKeysMatchingCorrectRootKey')
|
|
|
|
/** Stored version of items key should use new root key */
|
|
const stored = (await appA.deviceInterface.getAllDatabaseEntries(appA.identifier)).find(
|
|
(payload) => payload.uuid === newDefaultKey.uuid,
|
|
)
|
|
const storedParams = await appA.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(stored))
|
|
|
|
const correctStored = (await appB.deviceInterface.getAllDatabaseEntries(appB.identifier)).find(
|
|
(payload) => payload.uuid === newDefaultKey.uuid,
|
|
)
|
|
|
|
const correctParams = await appB.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(correctStored))
|
|
|
|
expect(storedParams).to.eql(correctParams)
|
|
|
|
await contextA.deinit()
|
|
await contextB.deinit()
|
|
}).timeout(80000)
|
|
})
|