Honor messageKeysLimit, remove batching for session saves

This commit is contained in:
Scott Nonnenberg 2021-02-08 14:19:35 -08:00
parent 80871270c6
commit 9858ae0642
7 changed files with 87 additions and 36 deletions

View file

@ -24,12 +24,14 @@ const {
const REVOKED_CERTIFICATES = []; const REVOKED_CERTIFICATES = [];
function SecretSessionCipher(storage) { function SecretSessionCipher(storage, options) {
this.storage = storage; this.storage = storage;
// We do this on construction because libsignal won't be available when this file loads // We do this on construction because libsignal won't be available when this file loads
const { SessionCipher } = libsignal; const { SessionCipher } = libsignal;
this.SessionCipher = SessionCipher; this.SessionCipher = SessionCipher;
this.options = options || {};
} }
const CIPHERTEXT_VERSION = 1; const CIPHERTEXT_VERSION = 1;
@ -291,7 +293,8 @@ SecretSessionCipher.prototype = {
const sessionCipher = new SessionCipher( const sessionCipher = new SessionCipher(
signalProtocolStore, signalProtocolStore,
destinationAddress destinationAddress,
this.options
); );
const message = await sessionCipher.encrypt(paddedPlaintext); const message = await sessionCipher.encrypt(paddedPlaintext);
@ -448,7 +451,11 @@ SecretSessionCipher.prototype = {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress); const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.getSessionVersion(); return cipher.getSessionVersion();
}, },
@ -458,7 +465,11 @@ SecretSessionCipher.prototype = {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress); const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.getRemoteRegistrationId(); return cipher.getRemoteRegistrationId();
}, },
@ -468,7 +479,11 @@ SecretSessionCipher.prototype = {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress); const cipher = new SessionCipher(
signalProtocolStore,
remoteAddress,
this.options
);
return cipher.closeOpenSessionForDevice(); return cipher.closeOpenSessionForDevice();
}, },
@ -528,12 +543,14 @@ SecretSessionCipher.prototype = {
case CiphertextMessage.WHISPER_TYPE: case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher( return new SessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender,
this.options
).decryptWhisperMessage(message.content); ).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE: case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher( return new SessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender,
this.options
).decryptPreKeyWhisperMessage(message.content); ).decryptPreKeyWhisperMessage(message.content);
default: default:
throw new Error(`Unknown type: ${message.type}`); throw new Error(`Unknown type: ${message.type}`);

View file

@ -163,20 +163,7 @@
} }
} }
function SignalProtocolStore() { 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);
},
});
}
async function _hydrateCache(object, field, itemsPromise, idField) { async function _hydrateCache(object, field, itemsPromise, idField) {
const items = await itemsPromise; const items = await itemsPromise;
@ -350,8 +337,11 @@
if (session) { if (session) {
return session.record; return session.record;
} }
} catch (e) { } catch (error) {
window.log.error(`could not load session ${encodedAddress}`); const errorString = error && error.stack ? error.stack : error;
window.log.error(
`could not load session ${encodedAddress}: ${errorString}`
);
} }
return undefined; return undefined;
@ -365,6 +355,7 @@
try { try {
const id = await normalizeEncodedAddress(encodedAddress); const id = await normalizeEncodedAddress(encodedAddress);
const previousData = this.sessions[id];
const data = { const data = {
id, id,
@ -373,13 +364,22 @@
record, record,
}; };
// Optimistically update in-memory cache; will revert if save fails.
this.sessions[id] = data; this.sessions[id] = data;
// Note: Because these are cached in memory, we batch and make these database try {
// updates out of band. await window.Signal.Data.createOrUpdateSession(data);
this.sessionUpdateBatcher.add(data); } catch (e) {
} catch (e) { if (previousData) {
window.log.error(`could not store session for ${encodedAddress}`); 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) { async getDeviceIds(identifier) {
@ -604,8 +604,21 @@
}, },
async _saveIdentityKey(data) { async _saveIdentityKey(data) {
const { id } = data; const { id } = data;
const previousData = this.identityKeys[id];
// Optimistically update in-memory cache; will revert if save fails.
this.identityKeys[id] = data; 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) { async saveIdentity(encodedAddress, publicKey, nonblockingApproval) {
if (encodedAddress === null || encodedAddress === undefined) { if (encodedAddress === null || encodedAddress === undefined) {

View file

@ -24703,9 +24703,10 @@ libsignal.SessionBuilder = function (storage, remoteAddress) {
this.processV3 = builder.processV3.bind(builder); this.processV3 = builder.processV3.bind(builder);
}; };
function SessionCipher(storage, remoteAddress) { function SessionCipher(storage, remoteAddress, options) {
this.remoteAddress = remoteAddress; this.remoteAddress = remoteAddress;
this.storage = storage; this.storage = storage;
this.options = options || {};
} }
SessionCipher.prototype = { SessionCipher.prototype = {
@ -25045,10 +25046,20 @@ SessionCipher.prototype = {
return Promise.resolve(); // Already calculated return Promise.resolve(); // Already calculated
} }
if (counter - chain.chainKey.counter > 5000) { var limit = 5000;
throw new Error('Over 5000 messages into the future! New: ' + counter + ', Existing: ' + chain.chainKey.counter); 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) { if (chain.chainKey.key === undefined) {
throw new Error("Got invalid request to extend chain after it was already closed"); throw new Error("Got invalid request to extend chain after it was already closed");
} }

2
ts/libsignal.d.ts vendored
View file

@ -202,7 +202,7 @@ export declare class SessionCipherClass {
constructor( constructor(
storage: StorageType, storage: StorageType,
remoteAddress: SignalProtocolAddressClass, remoteAddress: SignalProtocolAddressClass,
options?: any options?: { messageKeysLimit?: number | boolean }
); );
closeOpenSessionForDevice: () => Promise<void>; closeOpenSessionForDevice: () => Promise<void>;
decryptPreKeyWhisperMessage: ( decryptPreKeyWhisperMessage: (

View file

@ -937,7 +937,8 @@ class MessageReceiverInner extends EventTarget {
options options
); );
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
window.textsecure.storage.protocol window.textsecure.storage.protocol,
options
); );
const me = { const me = {
@ -1096,6 +1097,12 @@ class MessageReceiverInner extends EventTarget {
error.identityKey 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'); const ev = new Event('error');
ev.error = errorToThrow; ev.error = errorToThrow;
ev.proto = envelope; ev.proto = envelope;

View file

@ -285,7 +285,7 @@
"rule": "jQuery-load(", "rule": "jQuery-load(",
"path": "js/signal_protocol_store.js", "path": "js/signal_protocol_store.js",
"line": " await ConversationController.load();", "line": " await ConversationController.load();",
"lineNumber": 1022, "lineNumber": 1035,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-06-12T14:20:09.936Z" "updated": "2020-06-12T14:20:09.936Z"
}, },

5
ts/window.d.ts vendored
View file

@ -557,7 +557,10 @@ export class CertificateValidatorType {
} }
export class SecretSessionCipherClass { export class SecretSessionCipherClass {
constructor(storage: StorageType); constructor(
storage: StorageType,
options?: { messageKeysLimit?: number | boolean }
);
decrypt: ( decrypt: (
validator: CertificateValidatorType, validator: CertificateValidatorType,
ciphertext: ArrayBuffer, ciphertext: ArrayBuffer,