From 2f5a9f13d1f528d2204851e17bdfc42385f675fd Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 5 Dec 2018 13:21:06 +0100 Subject: [PATCH] handle state decrption error (#1699) --- CHANGELOG.md | 1 + app/src/renderer/vuex/store.js | 72 ++++++++++++++++------------- test/unit/specs/store/store.spec.js | 72 ++++++++++++++--------------- 3 files changed, 76 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76393aa23e..5e197172af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - [\#1640](https://github.com/cosmos/voyager/issues/1640) Fixed an error that prevented the search bar to be displayed using `Ctrl+F` @fedekunze - Fixed testnet config build script @faboweb - [\#1677](https://github.com/cosmos/voyager/issues/1677) Fixed inconstistent status colors on proposals @fedekunze +- [\#1687](https://github.com/cosmos/voyager/issues/1687) Removing cached state if decrypting fails. @faboweb - [\#1662](https://github.com/cosmos/voyager/issues/1662) Fixed wrong node version in readme @faboweb ## [0.10.7] - 2018-10-10 diff --git a/app/src/renderer/vuex/store.js b/app/src/renderer/vuex/store.js index c18ae1b434..22c31fe535 100644 --- a/app/src/renderer/vuex/store.js +++ b/app/src/renderer/vuex/store.js @@ -11,8 +11,10 @@ import Raven from "raven-js" Vue.use(Vuex) export default (opts = {}) => { + // provide commit and dispatch to tests opts.commit = (...args) => store.commit(...args) opts.dispatch = (...args) => store.dispatch(...args) + const store = new Vuex.Store({ getters, // strict: true, @@ -98,39 +100,47 @@ function getStorageKey(state) { } function loadPersistedState({ state, commit }, { password }) { - const cachedState = localStorage.getItem(getStorageKey(state)) + const storageKey = getStorageKey(state) + const cachedState = localStorage.getItem(storageKey) if (cachedState) { - const bytes = CryptoJS.AES.decrypt(cachedState, password) - const plaintext = bytes.toString(CryptoJS.enc.Utf8) - - // Replace the state object with the stored state - let oldState = JSON.parse(plaintext) - _.merge(state, oldState, { - // set loading indicators to false - transactions: { - loaded: true, - loading: false - }, - wallet: { - loaded: true, - loading: false - }, - delegates: { - loaded: true, - loading: false - }, - proposals: { - loaded: true, - loading: false - } - }) - this.replaceState(state) + try { + const bytes = CryptoJS.AES.decrypt(cachedState, password) + const plaintext = bytes.toString(CryptoJS.enc.Utf8) - // add all delegates the user has bond with already to the cart - state.delegates.delegates - .filter(d => state.delegation.committedDelegates[d.operator_address]) - .forEach(d => { - commit(`addToCart`, d) + // Replace the state object with the stored state + let oldState = JSON.parse(plaintext) + _.merge(state, oldState, { + // set loading indicators to false + transactions: { + loaded: true, + loading: false + }, + wallet: { + loaded: true, + loading: false + }, + delegates: { + loaded: true, + loading: false + }, + proposals: { + loaded: true, + loading: false + } }) + this.replaceState(state) + + // add all delegates the user has bond with already to the cart + state.delegates.delegates + .filter(d => state.delegation.committedDelegates[d.operator_address]) + .forEach(d => { + commit(`addToCart`, d) + }) + } catch (error) { + console.error(`Decrypting the state failed, removing cached state.`) + Raven.captureException(error) + // if decrypting the state fails, we cleanup + localStorage.removeItem(storageKey) + } } } diff --git a/test/unit/specs/store/store.spec.js b/test/unit/specs/store/store.spec.js index 09b7d3304f..910ca0377c 100644 --- a/test/unit/specs/store/store.spec.js +++ b/test/unit/specs/store/store.spec.js @@ -5,7 +5,7 @@ import lcdClientMock from "renderer/connectors/lcdClientMock.js" describe(`Store`, () => { let store - beforeEach(() => { + beforeEach(async () => { node.queryAccount = () => new Promise(() => {}) // make balances not return node.txs = () => new Promise(() => {}) // make txs not return store = Store({ node }) @@ -13,11 +13,7 @@ describe(`Store`, () => { `store_test-net_` + lcdClientMock.addresses[0], undefined ) - }) - - // DEFAULT - it(`should persist balances et al if the user is logged in`, async () => { jest.useFakeTimers() await store.dispatch(`setLastHeader`, { height: 42, @@ -27,6 +23,11 @@ describe(`Store`, () => { account: `default`, password: `1234567890` }) + }) + + // DEFAULT + + it(`should persist balances et al if the user is logged in`, async () => { store.commit(`setWalletBalances`, [{ denom: `fabocoin`, amount: 42 }]) jest.runAllTimers() // updating is waiting if more updates coming in, this skips the waiting expect( @@ -35,11 +36,7 @@ describe(`Store`, () => { }) it(`should not update cache if not logged in`, async () => { - jest.useFakeTimers() - await store.dispatch(`setLastHeader`, { - height: 42, - chain_id: `test-net` - }) + await store.dispatch(`signOut`) store.commit(`setWalletBalances`, [{ denom: `fabocoin`, amount: 42 }]) jest.runAllTimers() // updating is waiting if more updates coming in, this skips the waiting expect( @@ -48,15 +45,6 @@ describe(`Store`, () => { }) it(`should restore balances et al after logging in`, async () => { - jest.useFakeTimers() - await store.dispatch(`setLastHeader`, { - height: 42, - chain_id: `test-net` - }) - await store.dispatch(`signIn`, { - account: `default`, - password: `1234567890` - }) store.commit(`setWalletBalances`, [{ denom: `fabocoin`, amount: 42 }]) store.commit(`setWalletTxs`, [{}]) jest.runAllTimers() // updating is waiting if more updates coming in, this skips the waiting @@ -71,15 +59,6 @@ describe(`Store`, () => { }) it(`should restore delegates and put committed ones in the cart`, async () => { - jest.useFakeTimers() - await store.dispatch(`setLastHeader`, { - height: 42, - chain_id: `test-net` - }) - await store.dispatch(`signIn`, { - account: `default`, - password: `1234567890` - }) store.commit(`setDelegates`, lcdClientMock.state.candidates) store.commit(`setCommittedDelegation`, { candidateId: lcdClientMock.validators[0], @@ -120,15 +99,6 @@ describe(`Store`, () => { }) it(`should throttle updating the store cache`, async () => { - jest.useFakeTimers() - await store.dispatch(`setLastHeader`, { - height: 42, - chain_id: `test-net` - }) - await store.dispatch(`signIn`, { - account: `default`, - password: `1234567890` - }) store.commit(`setWalletBalances`, [{ denom: `fabocoin`, amount: 42 }]) // not updating yet, as it waits if there are more updates incoming @@ -145,7 +115,6 @@ describe(`Store`, () => { it(`should remove the cache if failing to encrypt the cache`, async () => { jest.resetModules() - jest.useFakeTimers() jest.doMock(`crypto-js`, () => ({ AES: { encrypt: () => { @@ -172,4 +141,31 @@ describe(`Store`, () => { expect(spy).toHaveBeenCalled() console.error.mockReset() }) + + it(`should remove the cache if failing to decrypt the cache`, async () => { + jest.resetModules() + jest.useFakeTimers() + jest.doMock(`crypto-js`, () => ({ + AES: { + decrypt: () => { + throw Error(`Failed to encrypt`) + } + } + })) + let Raven = require(`raven-js`) + // the error will be logged, which is confusing in the test output + jest.spyOn(console, `error`).mockImplementation(() => {}) + + let spy = jest.spyOn(Raven, `captureException`) + let opts = { node: { keys: { get: () => ({}) } } } + require(`renderer/vuex/store.js`).default(opts) + + // the store key is store__null if neither the chain_id or the address is set + localStorage.setItem(`store__null`, `xxx`) + await opts.dispatch(`loadPersistedState`, { password: `123` }) + jest.runAllTimers() + + expect(spy).toHaveBeenCalled() + console.error.mockReset() + }) })