Skip to content

Commit 111d6da

Browse files
committed
e2ee: save the account after generating new keys in a separate transaction
We're using applicative transactions to make sure that the account is properly synchronized in the cache vs in the database. Before this commit, the transaction would be committed only when *all* the operations in it succeeded. This was based on the assumption that most encryption requests could be replayed, by re-sending them to the server. Unfortunately, this assumption doesn't hold for when generating one-time keys: it could be that one time-keys would be generated by the client, then the applicative transaction would fail, resulting in the client "forgetting" about the one time keys it uploaded. The server rejects reuploads of existing one-time keys, so that would end up wedging a device, causing unable-to-decrypt events, without a proper way out. Here, we propose to save the account just after one-time keys have been generated.
1 parent 4c0c482 commit 111d6da

File tree

4 files changed

+33
-26
lines changed

4 files changed

+33
-26
lines changed

crates/matrix-sdk-crypto/src/dehydrated_devices.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,8 @@ impl RehydratedDevice {
240240

241241
// Let us first give the events to the rehydrated device, this will decrypt any
242242
// encrypted to-device events and fetch out the room keys.
243-
let mut rehydrated_transaction = self.rehydrated.store().transaction().await;
244-
245-
let (_, changes) = self
246-
.rehydrated
247-
.preprocess_sync_changes(&mut rehydrated_transaction, sync_changes)
248-
.await?;
243+
let (rehydrated_transaction, _, changes) =
244+
self.rehydrated.preprocess_sync_changes(sync_changes).await?;
249245

250246
// Now take the room keys and persist them in our original `OlmMachine`.
251247
let room_keys = &changes.inbound_group_sessions;

crates/matrix-sdk-crypto/src/machine.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,10 +1197,8 @@ impl OlmMachine {
11971197
&self,
11981198
sync_changes: EncryptionSyncChanges<'_>,
11991199
) -> OlmResult<(Vec<Raw<AnyToDeviceEvent>>, Vec<RoomKeyInfo>)> {
1200-
let mut store_transaction = self.inner.store.transaction().await;
1201-
1202-
let (events, changes) =
1203-
self.preprocess_sync_changes(&mut store_transaction, sync_changes).await?;
1200+
let (store_transaction, events, changes) =
1201+
self.preprocess_sync_changes(sync_changes).await?;
12041202

12051203
// Technically save_changes also does the same work, so if it's slow we could
12061204
// refactor this to do it only once.
@@ -1215,23 +1213,27 @@ impl OlmMachine {
12151213

12161214
pub(crate) async fn preprocess_sync_changes(
12171215
&self,
1218-
transaction: &mut StoreTransaction,
12191216
sync_changes: EncryptionSyncChanges<'_>,
1220-
) -> OlmResult<(Vec<Raw<AnyToDeviceEvent>>, Changes)> {
1217+
) -> OlmResult<(StoreTransaction, Vec<Raw<AnyToDeviceEvent>>, Changes)> {
12211218
// Remove verification objects that have expired or are done.
12221219
let mut events = self.inner.verification_machine.garbage_collect();
12231220

1224-
// The account is automatically saved by the store transaction created by the
1225-
// caller.
1226-
let mut changes = Default::default();
1221+
// Updating key counts may generate new one-time keys, and we should *never*
1222+
// forget about them once generated (otherwise this leads to issues like
1223+
// #1415), so save them in a separate transaction here.
1224+
self.inner
1225+
.store
1226+
.with_transaction(|mut transaction| async {
1227+
let account = transaction.account().await.map_err(OlmError::Store)?;
1228+
account.update_key_counts(
1229+
sync_changes.one_time_keys_counts,
1230+
sync_changes.unused_fallback_keys,
1231+
);
1232+
Ok((transaction, ()))
1233+
})
1234+
.await?;
12271235

1228-
{
1229-
let account = transaction.account().await?;
1230-
account.update_key_counts(
1231-
sync_changes.one_time_keys_counts,
1232-
sync_changes.unused_fallback_keys,
1233-
)
1234-
}
1236+
let mut transaction = self.inner.store.transaction().await;
12351237

12361238
if let Err(e) = self
12371239
.inner
@@ -1245,9 +1247,10 @@ impl OlmMachine {
12451247
error!(error = ?e, "Error marking a tracked user as changed");
12461248
}
12471249

1250+
let mut changes = Default::default();
12481251
for raw_event in sync_changes.to_device_events {
12491252
let raw_event =
1250-
Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event))
1253+
Box::pin(self.receive_to_device_event(&mut transaction, &mut changes, raw_event))
12511254
.await?;
12521255
events.push(raw_event);
12531256
}
@@ -1261,7 +1264,7 @@ impl OlmMachine {
12611264
changes.sessions.extend(changed_sessions);
12621265
changes.next_batch_token = sync_changes.next_batch_token;
12631266

1264-
Ok((events, changes))
1267+
Ok((transaction, events, changes))
12651268
}
12661269

12671270
/// Request a room key from our devices.

crates/matrix-sdk-crypto/src/olm/account.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,12 @@ impl Account {
473473
self.inner.max_number_of_one_time_keys()
474474
}
475475

476+
/// Generate new one-time keys if needed, and update the key counts
477+
/// accordingly.
478+
///
479+
/// Note: the `Account` *must* be persisted to disk after this has been
480+
/// called, otherwise it's possible that one-time keys get uploaded and
481+
/// the client forgets about those later.
476482
pub(crate) fn update_key_counts(
477483
&mut self,
478484
one_time_key_counts: &BTreeMap<DeviceKeyAlgorithm, UInt>,

crates/matrix-sdk-crypto/src/store/mod.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,17 @@ impl StoreTransaction {
449449

450450
/// Commits all dirty fields to the store, and maintains the cache so it
451451
/// reflects the current state of the database.
452-
pub async fn commit(self) -> Result<()> {
452+
pub async fn commit(mut self) -> Result<()> {
453453
if self.changes.is_empty() {
454454
return Ok(());
455455
}
456456

457457
// Save changes in the database.
458458
let account = self.changes.account.as_ref().map(|acc| acc.deep_clone());
459459

460-
self.store.save_pending_changes(self.changes).await?;
460+
// Note: after this `self.changes` is empty, and will be refilled as usual on
461+
// the next attempt to read data from it.
462+
self.store.save_pending_changes(std::mem::take(&mut self.changes)).await?;
461463

462464
// Make the cache coherent with the database.
463465
if let Some(account) = account {

0 commit comments

Comments
 (0)