diff --git a/js/modules/metadata/SecretSessionCipher.js b/js/modules/metadata/SecretSessionCipher.js index 4873a0e19..b4c468475 100644 --- a/js/modules/metadata/SecretSessionCipher.js +++ b/js/modules/metadata/SecretSessionCipher.js @@ -24,12 +24,14 @@ const { const REVOKED_CERTIFICATES = []; -function SecretSessionCipher(storage) { +function SecretSessionCipher(storage, options) { this.storage = storage; // We do this on construction because libsignal won't be available when this file loads const { SessionCipher } = libsignal; this.SessionCipher = SessionCipher; + + this.options = options || {}; } const CIPHERTEXT_VERSION = 1; @@ -291,7 +293,8 @@ SecretSessionCipher.prototype = { const sessionCipher = new SessionCipher( signalProtocolStore, - destinationAddress + destinationAddress, + this.options ); const message = await sessionCipher.encrypt(paddedPlaintext); @@ -448,7 +451,11 @@ SecretSessionCipher.prototype = { const { SessionCipher } = this; const signalProtocolStore = this.storage; - const cipher = new SessionCipher(signalProtocolStore, remoteAddress); + const cipher = new SessionCipher( + signalProtocolStore, + remoteAddress, + this.options + ); return cipher.getSessionVersion(); }, @@ -458,7 +465,11 @@ SecretSessionCipher.prototype = { const { SessionCipher } = this; const signalProtocolStore = this.storage; - const cipher = new SessionCipher(signalProtocolStore, remoteAddress); + const cipher = new SessionCipher( + signalProtocolStore, + remoteAddress, + this.options + ); return cipher.getRemoteRegistrationId(); }, @@ -468,7 +479,11 @@ SecretSessionCipher.prototype = { const { SessionCipher } = this; const signalProtocolStore = this.storage; - const cipher = new SessionCipher(signalProtocolStore, remoteAddress); + const cipher = new SessionCipher( + signalProtocolStore, + remoteAddress, + this.options + ); return cipher.closeOpenSessionForDevice(); }, @@ -528,12 +543,14 @@ SecretSessionCipher.prototype = { case CiphertextMessage.WHISPER_TYPE: return new SessionCipher( signalProtocolStore, - sender + sender, + this.options ).decryptWhisperMessage(message.content); case CiphertextMessage.PREKEY_TYPE: return new SessionCipher( signalProtocolStore, - sender + sender, + this.options ).decryptPreKeyWhisperMessage(message.content); default: throw new Error(`Unknown type: ${message.type}`); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 180704e5c..75bbc41ae 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -163,20 +163,7 @@ } } - function SignalProtocolStore() { - this.sessionUpdateBatcher = window.Signal.Util.createBatcher({ - wait: 500, - maxSize: 20, - processBatch: async items => { - // We only care about the most recent update for each session - const byId = _.groupBy(items, item => item.id); - const ids = Object.keys(byId); - const mostRecent = ids.map(id => _.last(byId[id])); - - await window.Signal.Data.createOrUpdateSessions(mostRecent); - }, - }); - } + function SignalProtocolStore() {} async function _hydrateCache(object, field, itemsPromise, idField) { const items = await itemsPromise; @@ -350,8 +337,11 @@ if (session) { return session.record; } - } catch (e) { - window.log.error(`could not load session ${encodedAddress}`); + } catch (error) { + const errorString = error && error.stack ? error.stack : error; + window.log.error( + `could not load session ${encodedAddress}: ${errorString}` + ); } return undefined; @@ -365,6 +355,7 @@ try { const id = await normalizeEncodedAddress(encodedAddress); + const previousData = this.sessions[id]; const data = { id, @@ -373,13 +364,22 @@ record, }; + // Optimistically update in-memory cache; will revert if save fails. this.sessions[id] = data; - // Note: Because these are cached in memory, we batch and make these database - // updates out of band. - this.sessionUpdateBatcher.add(data); - } catch (e) { - window.log.error(`could not store session for ${encodedAddress}`); + try { + await window.Signal.Data.createOrUpdateSession(data); + } catch (e) { + if (previousData) { + this.sessions[id] = previousData; + } + throw e; + } + } catch (error) { + const errorString = error && error.stack ? error.stack : error; + window.log.error( + `could not store session for ${encodedAddress}: ${errorString}` + ); } }, async getDeviceIds(identifier) { @@ -604,8 +604,21 @@ }, async _saveIdentityKey(data) { const { id } = data; + + const previousData = this.identityKeys[id]; + + // Optimistically update in-memory cache; will revert if save fails. this.identityKeys[id] = data; - await window.Signal.Data.createOrUpdateIdentityKey(data); + + try { + await window.Signal.Data.createOrUpdateIdentityKey(data); + } catch (error) { + if (previousData) { + this.identityKeys[id] = previousData; + } + + throw error; + } }, async saveIdentity(encodedAddress, publicKey, nonblockingApproval) { if (encodedAddress === null || encodedAddress === undefined) { diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index 802af05a7..28831add4 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -24703,9 +24703,10 @@ libsignal.SessionBuilder = function (storage, remoteAddress) { this.processV3 = builder.processV3.bind(builder); }; -function SessionCipher(storage, remoteAddress) { +function SessionCipher(storage, remoteAddress, options) { this.remoteAddress = remoteAddress; this.storage = storage; + this.options = options || {}; } SessionCipher.prototype = { @@ -25045,10 +25046,20 @@ SessionCipher.prototype = { return Promise.resolve(); // Already calculated } - if (counter - chain.chainKey.counter > 5000) { - throw new Error('Over 5000 messages into the future! New: ' + counter + ', Existing: ' + chain.chainKey.counter); + var limit = 5000; + if (this.options.messageKeysLimit === false) { + // noop + } else { + if (this.options.messageKeysLimit > 0) { + limit = this.options.messageKeysLimit; + } + + if (counter - chain.chainKey.counter > limit) { + throw new Error('Over ' + limit + ' messages into the future! New: ' + counter + ', Existing: ' + chain.chainKey.counter); + } } + if (chain.chainKey.key === undefined) { throw new Error("Got invalid request to extend chain after it was already closed"); } diff --git a/ts/libsignal.d.ts b/ts/libsignal.d.ts index aaf2e0fe1..e348c8ecc 100644 --- a/ts/libsignal.d.ts +++ b/ts/libsignal.d.ts @@ -202,7 +202,7 @@ export declare class SessionCipherClass { constructor( storage: StorageType, remoteAddress: SignalProtocolAddressClass, - options?: any + options?: { messageKeysLimit?: number | boolean } ); closeOpenSessionForDevice: () => Promise; decryptPreKeyWhisperMessage: ( diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index c1b1398ed..8d07fb0e1 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -937,7 +937,8 @@ class MessageReceiverInner extends EventTarget { options ); const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( - window.textsecure.storage.protocol + window.textsecure.storage.protocol, + options ); const me = { @@ -1096,6 +1097,12 @@ class MessageReceiverInner extends EventTarget { error.identityKey ); } + + if (envelope.timestamp && envelope.timestamp.toNumber) { + // eslint-disable-next-line no-param-reassign + envelope.timestamp = envelope.timestamp.toNumber(); + } + const ev = new Event('error'); ev.error = errorToThrow; ev.proto = envelope; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 9bca96370..ac33926bf 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -285,7 +285,7 @@ "rule": "jQuery-load(", "path": "js/signal_protocol_store.js", "line": " await ConversationController.load();", - "lineNumber": 1022, + "lineNumber": 1035, "reasonCategory": "falseMatch", "updated": "2020-06-12T14:20:09.936Z" }, diff --git a/ts/window.d.ts b/ts/window.d.ts index 35fa3590b..1325cbd6b 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -557,7 +557,10 @@ export class CertificateValidatorType { } export class SecretSessionCipherClass { - constructor(storage: StorageType); + constructor( + storage: StorageType, + options?: { messageKeysLimit?: number | boolean } + ); decrypt: ( validator: CertificateValidatorType, ciphertext: ArrayBuffer,