fix: Deduplicate key OTK uploads

We do the key upload asynchronously without awaiting it. This means we
may do multiple syncs before the key upload finishes. So we may generate
more keys than we should.

To fix that prevent multiple key uploads from running at once. This may
lead to outdated key uploads in some cases if we miss an OTK being used
in sync. However, the next sync will still tell us about that so in the
worst case this might delay key uploads by 30s (with the default sync
timeout), which for normal usage should be completely acceptable.
This commit is contained in:
Nicolas Werner 2024-10-07 19:05:54 +02:00
parent 583be5ece7
commit 6a28ab05d0
No known key found for this signature in database
GPG Key ID: B38119FF80087618
1 changed files with 36 additions and 28 deletions

View File

@ -28,6 +28,7 @@ import 'package:matrix/encryption/utils/json_signature_check_extension.dart';
import 'package:matrix/encryption/utils/olm_session.dart'; import 'package:matrix/encryption/utils/olm_session.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/api.dart'; import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/api.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';
import 'package:matrix/src/utils/run_in_root.dart'; import 'package:matrix/src/utils/run_in_root.dart';
class OlmManager { class OlmManager {
@ -326,43 +327,50 @@ class OlmManager {
return false; return false;
} }
final _otkUpdateDedup = AsyncCache<void>.ephemeral();
Future<void> handleDeviceOneTimeKeysCount( Future<void> handleDeviceOneTimeKeysCount(
Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) async { Map<String, int>? countJson, List<String>? unusedFallbackKeyTypes) async {
if (!enabled) { if (!enabled) {
return; return;
} }
final haveFallbackKeys = encryption.isMinOlmVersion(3, 2, 0);
// Check if there are at least half of max_number_of_one_time_keys left on the server
// and generate and upload more if not.
// If the server did not send us a count, assume it is 0 await _otkUpdateDedup.fetch(() =>
final keyCount = countJson?.tryGet<int>('signed_curve25519') ?? 0; runBenchmarked('handleOtkUpdate', () async {
final haveFallbackKeys = encryption.isMinOlmVersion(3, 2, 0);
// Check if there are at least half of max_number_of_one_time_keys left on the server
// and generate and upload more if not.
// If the server does not support fallback keys, it will not tell us about them. // If the server did not send us a count, assume it is 0
// If the server supports them but has no key, upload a new one. final keyCount = countJson?.tryGet<int>('signed_curve25519') ?? 0;
var unusedFallbackKey = true;
if (unusedFallbackKeyTypes?.contains('signed_curve25519') == false) {
unusedFallbackKey = false;
}
// fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway. // If the server does not support fallback keys, it will not tell us about them.
if (keyCount > _olmAccount!.max_number_of_one_time_keys()) { // If the server supports them but has no key, upload a new one.
final requestingKeysFrom = { var unusedFallbackKey = true;
client.userID!: {ourDeviceId!: 'signed_curve25519'} if (unusedFallbackKeyTypes?.contains('signed_curve25519') == false) {
}; unusedFallbackKey = false;
await client.claimKeys(requestingKeysFrom, timeout: 10000); }
}
// Only upload keys if they are less than half of the max or we have no unused fallback key // fixup accidental too many uploads. We delete only one of them so that the server has time to update the counts and because we will get rate limited anyway.
if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) || if (keyCount > _olmAccount!.max_number_of_one_time_keys()) {
!unusedFallbackKey) { final requestingKeysFrom = {
await uploadKeys( client.userID!: {ourDeviceId!: 'signed_curve25519'}
oldKeyCount: keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) };
? keyCount await client.claimKeys(requestingKeysFrom, timeout: 10000);
: null, }
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
); // Only upload keys if they are less than half of the max or we have no unused fallback key
} if (keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2) ||
!unusedFallbackKey) {
await uploadKeys(
oldKeyCount:
keyCount < (_olmAccount!.max_number_of_one_time_keys() / 2)
? keyCount
: null,
unusedFallbackKey: haveFallbackKeys ? unusedFallbackKey : null,
);
}
}));
} }
Future<void> storeOlmSession(OlmSession session) async { Future<void> storeOlmSession(OlmSession session) async {