From 9c540ab97784053f07349796cc6631d09b281801 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 4 Feb 2019 15:54:37 -0800 Subject: [PATCH] Add cache support to Signal Protocol Store --- app/sql.js | 17 +++ js/background.js | 5 +- js/modules/data.js | 17 ++- js/signal_protocol_store.js | 151 ++++++++++++++++++--------- test/keychange_listener_test.js | 3 +- test/storage_test.js | 28 +++++ ts/components/conversation/Image.tsx | 2 +- 7 files changed, 171 insertions(+), 52 deletions(-) diff --git a/app/sql.js b/app/sql.js index 49472d73f..1d3cf9325 100644 --- a/app/sql.js +++ b/app/sql.js @@ -29,12 +29,14 @@ module.exports = { bulkAddIdentityKeys, removeIdentityKeyById, removeAllIdentityKeys, + getAllIdentityKeys, createOrUpdatePreKey, getPreKeyById, bulkAddPreKeys, removePreKeyById, removeAllPreKeys, + getAllPreKeys, createOrUpdateSignedPreKey, getSignedPreKeyById, @@ -57,6 +59,7 @@ module.exports = { removeSessionById, removeSessionsByNumber, removeAllSessions, + getAllSessions, getConversationCount, saveConversation, @@ -701,6 +704,9 @@ async function removeIdentityKeyById(id) { async function removeAllIdentityKeys() { return removeAllFromTable(IDENTITY_KEYS_TABLE); } +async function getAllIdentityKeys() { + return getAllFromTable(IDENTITY_KEYS_TABLE); +} const PRE_KEYS_TABLE = 'preKeys'; async function createOrUpdatePreKey(data) { @@ -718,6 +724,9 @@ async function removePreKeyById(id) { async function removeAllPreKeys() { return removeAllFromTable(PRE_KEYS_TABLE); } +async function getAllPreKeys() { + return getAllFromTable(PRE_KEYS_TABLE); +} const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; async function createOrUpdateSignedPreKey(data) { @@ -815,6 +824,9 @@ async function removeSessionsByNumber(number) { async function removeAllSessions() { return removeAllFromTable(SESSIONS_TABLE); } +async function getAllSessions() { + return getAllFromTable(SESSIONS_TABLE); +} async function createOrUpdate(table, data) { const { id } = data; @@ -884,6 +896,11 @@ async function removeAllFromTable(table) { await db.run(`DELETE FROM ${table};`); } +async function getAllFromTable(table) { + const rows = await db.all(`SELECT json FROM ${table};`); + return rows.map(row => jsonToObject(row.json)); +} + // Conversations async function getConversationCount() { diff --git a/js/background.js b/js/background.js index f1000f0ce..2b9f07876 100644 --- a/js/background.js +++ b/js/background.js @@ -414,7 +414,10 @@ window.Events.setThemeSetting(newThemeSetting); try { - await ConversationController.load(); + await Promise.all([ + ConversationController.load(), + textsecure.storage.protocol.hydrateCaches(), + ]); } catch (error) { window.log.error( 'background.js: ConversationController failed to load:', diff --git a/js/modules/data.js b/js/modules/data.js index debfec629..1b93a8d76 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -60,12 +60,14 @@ module.exports = { bulkAddIdentityKeys, removeIdentityKeyById, removeAllIdentityKeys, + getAllIdentityKeys, createOrUpdatePreKey, getPreKeyById, bulkAddPreKeys, removePreKeyById, removeAllPreKeys, + getAllPreKeys, createOrUpdateSignedPreKey, getSignedPreKeyById, @@ -88,6 +90,7 @@ module.exports = { removeSessionById, removeSessionsByNumber, removeAllSessions, + getAllSessions, getConversationCount, saveConversation, @@ -440,6 +443,10 @@ async function removeIdentityKeyById(id) { async function removeAllIdentityKeys() { await channels.removeAllIdentityKeys(); } +async function getAllIdentityKeys() { + const keys = await channels.getAllIdentityKeys(); + return keys.map(key => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); +} // Pre Keys @@ -461,6 +468,10 @@ async function removePreKeyById(id) { async function removeAllPreKeys() { await channels.removeAllPreKeys(); } +async function getAllPreKeys() { + const keys = await channels.getAllPreKeys(); + return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); +} // Signed Pre Keys @@ -475,7 +486,7 @@ async function getSignedPreKeyById(id) { } async function getAllSignedPreKeys() { const keys = await channels.getAllSignedPreKeys(); - return keys; + return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); } async function bulkAddSignedPreKeys(array) { const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); @@ -567,6 +578,10 @@ async function removeSessionsByNumber(number) { async function removeAllSessions(id) { await channels.removeAllSessions(id); } +async function getAllSessions(id) { + const sessions = await channels.getAllSessions(id); + return sessions; +} // Conversation diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 50d3eebdc..ebbde8ceb 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -150,8 +150,51 @@ function SignalProtocolStore() {} + async function _hydrateCache(object, field, items, idField) { + const cache = Object.create(null); + for (let i = 0, max = items.length; i < max; i += 1) { + const item = items[i]; + const id = item[idField]; + + cache[id] = item; + } + + window.log.info(`SignalProtocolStore: Finished caching ${field} data`); + // eslint-disable-next-line no-param-reassign + object[field] = cache; + } + SignalProtocolStore.prototype = { constructor: SignalProtocolStore, + async hydrateCaches() { + await Promise.all([ + _hydrateCache( + this, + 'identityKeys', + await window.Signal.Data.getAllIdentityKeys(), + 'id' + ), + _hydrateCache( + this, + 'sessions', + await window.Signal.Data.getAllSessions(), + 'id' + ), + _hydrateCache( + this, + 'preKeys', + await window.Signal.Data.getAllPreKeys(), + 'id' + ), + _hydrateCache( + this, + 'signedPreKeys', + await window.Signal.Data.getAllSignedPreKeys(), + 'id' + ), + ]); + }, + async getIdentityKeyPair() { const item = await window.Signal.Data.getItemById('identityKey'); if (item) { @@ -169,9 +212,10 @@ return undefined; }, - /* Returns a prekeypair object or undefined */ + // PreKeys + async loadPreKey(keyId) { - const key = await window.Signal.Data.getPreKeyById(keyId); + const key = this.preKeys[keyId]; if (key) { window.log.info('Successfully fetched prekey:', keyId); return { @@ -190,6 +234,7 @@ privateKey: keyPair.privKey, }; + this.preKeys[keyId] = data; await window.Signal.Data.createOrUpdatePreKey(data); }, async removePreKey(keyId) { @@ -202,15 +247,18 @@ ); } + delete this.preKeys[keyId]; await window.Signal.Data.removePreKeyById(keyId); }, async clearPreKeyStore() { + this.preKeys = Object.create(null); await window.Signal.Data.removeAllPreKeys(); }, - /* Returns a signed keypair object or undefined */ + // Signed PreKeys + async loadSignedPreKey(keyId) { - const key = await window.Signal.Data.getSignedPreKeyById(keyId); + const key = this.signedPreKeys[keyId]; if (key) { window.log.info('Successfully fetched signed prekey:', key.id); return { @@ -230,7 +278,7 @@ throw new Error('loadSignedPreKeys takes no arguments'); } - const keys = await window.Signal.Data.getAllSignedPreKeys(); + const keys = Object.values(this.signedPreKeys); return keys.map(prekey => ({ pubKey: prekey.publicKey, privKey: prekey.privateKey, @@ -240,28 +288,34 @@ })); }, async storeSignedPreKey(keyId, keyPair, confirmed) { - const key = { + const data = { id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, created_at: Date.now(), confirmed: Boolean(confirmed), }; - await window.Signal.Data.createOrUpdateSignedPreKey(key); + + this.signedPreKeys[keyId] = data; + await window.Signal.Data.createOrUpdateSignedPreKey(data); }, async removeSignedPreKey(keyId) { + delete this.signedPreKeys[keyId]; await window.Signal.Data.removeSignedPreKeyById(keyId); }, async clearSignedPreKeysStore() { + this.signedPreKeys = Object.create(null); await window.Signal.Data.removeAllSignedPreKeys(); }, + // Sessions + async loadSession(encodedNumber) { if (encodedNumber === null || encodedNumber === undefined) { throw new Error('Tried to get session for undefined/null number'); } - const session = await window.Signal.Data.getSessionById(encodedNumber); + const session = this.sessions[encodedNumber]; if (session) { return session.record; } @@ -283,6 +337,7 @@ record, }; + this.sessions[encodedNumber] = data; await window.Signal.Data.createOrUpdateSession(data); }, async getDeviceIds(number) { @@ -290,11 +345,13 @@ throw new Error('Tried to get device ids for undefined/null number'); } - const sessions = await window.Signal.Data.getSessionsByNumber(number); + const allSessions = Object.values(this.sessions); + const sessions = allSessions.filter(session => session.number === number); return _.pluck(sessions, 'deviceId'); }, async removeSession(encodedNumber) { window.log.info('deleting session for ', encodedNumber); + delete this.sessions[encodedNumber]; await window.Signal.Data.removeSessionById(encodedNumber); }, async removeAllSessions(number) { @@ -302,6 +359,13 @@ throw new Error('Tried to remove sessions for undefined/null number'); } + const allSessions = Object.values(this.sessions); + for (let i = 0, max = allSessions.length; i < max; i += 1) { + const session = allSessions[i]; + if (session.number === number) { + delete this.sessions[session.id]; + } + } await window.Signal.Data.removeSessionsByNumber(number); }, async archiveSiblingSessions(identifier) { @@ -341,8 +405,12 @@ ); }, async clearSessionStore() { + this.sessions = Object.create(null); window.Signal.Data.removeAllSessions(); }, + + // Identity Keys + async isTrustedIdentity(identifier, publicKey, direction) { if (identifier === null || identifier === undefined) { throw new Error('Tried to get identity key for undefined/null key'); @@ -350,9 +418,7 @@ const number = textsecure.utils.unencodeNumber(identifier)[0]; const isOurNumber = number === textsecure.storage.user.getNumber(); - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; if (isOurNumber) { const existing = identityRecord ? identityRecord.publicKey : null; @@ -402,9 +468,7 @@ throw new Error('Tried to get identity key for undefined/null key'); } const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; if (identityRecord) { return identityRecord.publicKey; @@ -412,6 +476,11 @@ return undefined; }, + async _saveIdentityKey(data) { + const { id } = data; + this.identityKeys[id] = data; + await window.Signal.Data.createOrUpdateIdentityKey(data); + }, async saveIdentity(identifier, publicKey, nonblockingApproval) { if (identifier === null || identifier === undefined) { throw new Error('Tried to put identity key for undefined/null key'); @@ -426,14 +495,12 @@ } const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; if (!identityRecord || !identityRecord.publicKey) { // Lookup failed, or the current key was removed, so save this one. window.log.info('Saving new identity...'); - await window.Signal.Data.createOrUpdateIdentityKey({ + await this._saveIdentityKey({ id: number, publicKey, firstUse: true, @@ -459,7 +526,7 @@ verifiedStatus = VerifiedStatus.DEFAULT; } - await window.Signal.Data.createOrUpdateIdentityKey({ + await this._saveIdentityKey({ id: number, publicKey, firstUse: false, @@ -483,7 +550,7 @@ window.log.info('Setting approval status...'); identityRecord.nonblockingApproval = nonblockingApproval; - await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); + await this._saveIdentityKey(identityRecord); return false; } @@ -503,9 +570,7 @@ } const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; const updates = { id: number, @@ -515,7 +580,7 @@ const model = new IdentityRecord(updates); if (model.isValid()) { - await window.Signal.Data.createOrUpdateIdentityKey(updates); + await this._saveIdentityKey(updates); } else { throw model.validationError; } @@ -529,16 +594,14 @@ } const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; if (!identityRecord) { throw new Error(`No identity record for ${number}`); } identityRecord.nonblockingApproval = nonblockingApproval; - await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); + await this._saveIdentityKey(identityRecord); }, async setVerified(number, verifiedStatus, publicKey) { if (number === null || number === undefined) { @@ -551,9 +614,7 @@ throw new Error('Invalid public key'); } - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; if (!identityRecord) { throw new Error(`No identity record for ${number}`); } @@ -566,7 +627,7 @@ const model = new IdentityRecord(identityRecord); if (model.isValid()) { - await window.Signal.Data.createOrUpdateIdentityKey(identityRecord); + await this._saveIdentityKey(identityRecord); } else { throw identityRecord.validationError; } @@ -579,10 +640,7 @@ throw new Error('Tried to set verified for undefined/null key'); } - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); - + const identityRecord = this.identityKeys[number]; if (!identityRecord) { throw new Error(`No identity record for ${number}`); } @@ -616,9 +674,7 @@ throw new Error('Invalid public key'); } - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; const isPresent = Boolean(identityRecord); let isEqual = false; @@ -683,9 +739,7 @@ throw new Error('Invalid public key'); } - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); + const identityRecord = this.identityKeys[number]; const isPresent = Boolean(identityRecord); let isEqual = false; @@ -754,10 +808,7 @@ throw new Error('Tried to set verified for undefined/null key'); } - const identityRecord = await window.Signal.Data.getIdentityKeyById( - number - ); - + const identityRecord = this.identityKeys[number]; if (!identityRecord) { throw new Error(`No identity record for ${number}`); } @@ -773,11 +824,13 @@ return false; }, async removeIdentityKey(number) { + delete this.identityKeys[number]; await window.Signal.Data.removeIdentityKeyById(number); - return textsecure.storage.protocol.removeAllSessions(number); + await textsecure.storage.protocol.removeAllSessions(number); }, // Groups + async getGroup(groupId) { if (groupId === null || groupId === undefined) { throw new Error('Tried to get group for undefined/null id'); @@ -840,6 +893,7 @@ }, async removeAllData() { await window.Signal.Data.removeAll(); + await this.hydrateCaches(); window.storage.reset(); await window.storage.fetch(); @@ -849,6 +903,7 @@ }, async removeAllConfiguration() { await window.Signal.Data.removeAllConfiguration(); + await this.hydrateCaches(); window.storage.reset(); await window.storage.fetch(); diff --git a/test/keychange_listener_test.js b/test/keychange_listener_test.js index 08da00bf2..43a8c759b 100644 --- a/test/keychange_listener_test.js +++ b/test/keychange_listener_test.js @@ -10,8 +10,9 @@ describe('KeyChangeListener', () => { const newKey = libsignal.crypto.getRandomBytes(33); let store; - beforeEach(() => { + beforeEach(async () => { store = new SignalProtocolStore(); + await store.hydrateCaches(); Whisper.KeyChangeListener.init(store); return store.saveIdentity(address.toString(), oldKey); }); diff --git a/test/storage_test.js b/test/storage_test.js index 960aef9f5..1874915ed 100644 --- a/test/storage_test.js +++ b/test/storage_test.js @@ -10,6 +10,7 @@ describe('SignalProtocolStore', () => { before(done => { store = textsecure.storage.protocol; + store.hydrateCaches(); identityKey = { pubKey: libsignal.crypto.getRandomBytes(33), privKey: libsignal.crypto.getRandomBytes(32), @@ -86,6 +87,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.DEFAULT, }); + await store.hydrateCaches(); await store.saveIdentity(identifier, newIdentity); }); it('marks the key not firstUse', async () => { @@ -107,6 +109,7 @@ describe('SignalProtocolStore', () => { nonblockingApproval: false, verified: store.VerifiedStatus.DEFAULT, }); + await store.hydrateCaches(); await store.saveIdentity(identifier, newIdentity); }); @@ -125,6 +128,8 @@ describe('SignalProtocolStore', () => { nonblockingApproval: false, verified: store.VerifiedStatus.VERIFIED, }); + + await store.hydrateCaches(); await store.saveIdentity(identifier, newIdentity); }); it('sets the new key to unverified', async () => { @@ -147,6 +152,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.UNVERIFIED, }); + await store.hydrateCaches(); await store.saveIdentity(identifier, newIdentity); }); it('sets the new key to unverified', async () => { @@ -168,12 +174,14 @@ describe('SignalProtocolStore', () => { nonblockingApproval: false, verified: store.VerifiedStatus.DEFAULT, }); + await store.hydrateCaches(); }); describe('If it is marked firstUse', () => { before(async () => { const identity = await window.Signal.Data.getIdentityKeyById(number); identity.firstUse = true; await window.Signal.Data.createOrUpdateIdentityKey(identity); + await store.hydrateCaches(); }); it('nothing changes', async () => { await store.saveIdentity(identifier, testKey.pubKey, true); @@ -188,6 +196,7 @@ describe('SignalProtocolStore', () => { const identity = await window.Signal.Data.getIdentityKeyById(number); identity.firstUse = false; await window.Signal.Data.createOrUpdateIdentityKey(identity); + await store.hydrateCaches(); }); describe('If nonblocking approval is required', () => { let now; @@ -198,6 +207,7 @@ describe('SignalProtocolStore', () => { ); identity.timestamp = now; await window.Signal.Data.createOrUpdateIdentityKey(identity); + await store.hydrateCaches(); }); it('sets non-blocking approval', async () => { await store.saveIdentity(identifier, testKey.pubKey, true); @@ -311,6 +321,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); + await store.hydrateCaches(); } describe('with no public key argument', () => { before(saveRecordDefault); @@ -370,6 +381,7 @@ describe('SignalProtocolStore', () => { describe('when there is no existing record', () => { before(async () => { await window.Signal.Data.removeIdentityKeyById(number); + await store.hydrateCaches(); }); it('does nothing', async () => { @@ -403,6 +415,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('does not save the new identity (because this is a less secure state)', async () => { @@ -434,6 +447,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('updates the verified status', async () => { @@ -462,6 +476,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('does not hang', async () => { @@ -480,6 +495,7 @@ describe('SignalProtocolStore', () => { describe('when there is no existing record', () => { before(async () => { await window.Signal.Data.removeIdentityKeyById(number); + await store.hydrateCaches(); }); it('saves the new identity and marks it verified', async () => { @@ -510,6 +526,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('saves the new identity and marks it UNVERIFIED', async () => { @@ -541,6 +558,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.DEFAULT, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('updates the verified status', async () => { @@ -571,6 +589,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.UNVERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('does not hang', async () => { @@ -589,6 +608,7 @@ describe('SignalProtocolStore', () => { describe('when there is no existing record', () => { before(async () => { await window.Signal.Data.removeIdentityKeyById(number); + await store.hydrateCaches(); }); it('saves the new identity and marks it verified', async () => { @@ -615,6 +635,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('saves the new identity and marks it VERIFIED', async () => { @@ -646,6 +667,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.UNVERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('saves the identity and marks it verified', async () => { @@ -676,6 +698,7 @@ describe('SignalProtocolStore', () => { verified: store.VerifiedStatus.VERIFIED, nonblockingApproval: false, }); + await store.hydrateCaches(); }); it('does not hang', async () => { @@ -703,6 +726,7 @@ describe('SignalProtocolStore', () => { nonblockingApproval: false, }); + await store.hydrateCaches(); const untrusted = await store.isUntrusted(number); assert.strictEqual(untrusted, false); }); @@ -716,6 +740,7 @@ describe('SignalProtocolStore', () => { firstUse: false, nonblockingApproval: true, }); + await store.hydrateCaches(); const untrusted = await store.isUntrusted(number); assert.strictEqual(untrusted, false); @@ -730,6 +755,7 @@ describe('SignalProtocolStore', () => { firstUse: true, nonblockingApproval: false, }); + await store.hydrateCaches(); const untrusted = await store.isUntrusted(number); assert.strictEqual(untrusted, false); @@ -744,6 +770,8 @@ describe('SignalProtocolStore', () => { firstUse: false, nonblockingApproval: false, }); + await store.hydrateCaches(); + const untrusted = await store.isUntrusted(number); assert.strictEqual(untrusted, true); }); diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index 9b932b59d..e90659c3e 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -67,7 +67,7 @@ export class Image extends React.Component {
{ - if (canClick) { + if (canClick && onClick) { onClick(attachment); } }}