diff --git a/lib/encryption/encryption.dart b/lib/encryption/encryption.dart index 7d5de5e5c..bc98a47b7 100644 --- a/lib/encryption/encryption.dart +++ b/lib/encryption/encryption.dart @@ -68,15 +68,20 @@ class Encryption { } // initial login passes null to init a new olm account - Future init(String? olmAccount, - {String? deviceId, - String? pickleKey, - bool isDehydratedDevice = false}) async { + Future init( + String? olmAccount, { + String? deviceId, + String? pickleKey, + String? dehydratedDeviceAlgorithm, + }) async { ourDeviceId = deviceId ?? client.deviceID!; + final isDehydratedDevice = dehydratedDeviceAlgorithm != null; await olmManager.init( - olmAccount: olmAccount, - deviceId: isDehydratedDevice ? deviceId : ourDeviceId, - pickleKey: pickleKey); + olmAccount: olmAccount, + deviceId: isDehydratedDevice ? deviceId : ourDeviceId, + pickleKey: pickleKey, + dehydratedDeviceAlgorithm: dehydratedDeviceAlgorithm, + ); if (!isDehydratedDevice) keyManager.startAutoUploadKeys(); } diff --git a/lib/encryption/olm_manager.dart b/lib/encryption/olm_manager.dart index fca70d752..1e0e8bf15 100644 --- a/lib/encryption/olm_manager.dart +++ b/lib/encryption/olm_manager.dart @@ -57,10 +57,12 @@ class OlmManager { final Map> _olmSessions = {}; // NOTE(Nico): On initial login we pass null to create a new account - Future init( - {String? olmAccount, - required String? deviceId, - String? pickleKey}) async { + Future init({ + String? olmAccount, + required String? deviceId, + String? pickleKey, + String? dehydratedDeviceAlgorithm, + }) async { ourDeviceId = deviceId; if (olmAccount == null) { try { @@ -68,10 +70,12 @@ class OlmManager { _olmAccount = olm.Account(); _olmAccount!.create(); if (!await uploadKeys( - uploadDeviceKeys: true, - updateDatabase: false, - // dehydrated devices don't have a device id when created, so skip upload in that case. - skipAllUploads: deviceId == null)) { + uploadDeviceKeys: true, + updateDatabase: false, + dehydratedDeviceAlgorithm: dehydratedDeviceAlgorithm, + dehydratedDevicePickleKey: + dehydratedDeviceAlgorithm != null ? pickleKey : null, + )) { throw ('Upload key failed'); } } catch (_) { @@ -131,7 +135,8 @@ class OlmManager { int? oldKeyCount = 0, bool updateDatabase = true, bool? unusedFallbackKey = false, - bool skipAllUploads = false, + String? dehydratedDeviceAlgorithm, + String? dehydratedDevicePickleKey, int retry = 1, }) async { final olmAccount = _olmAccount; @@ -179,11 +184,6 @@ class OlmManager { await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!); } - if (skipAllUploads) { - _uploadKeysLock = false; - return true; - } - // and now generate the payload to upload var deviceKeys = { 'user_id': client.userID, @@ -239,23 +239,36 @@ class OlmManager { // Workaround: Make sure we stop if we got logged out in the meantime. if (!client.isLogged()) return true; - final currentUpload = this.currentUpload = - CancelableOperation.fromFuture(ourDeviceId == client.deviceID - ? client.uploadKeys( - deviceKeys: uploadDeviceKeys - ? MatrixDeviceKeys.fromJson(deviceKeys) - : null, - oneTimeKeys: signedOneTimeKeys, - fallbackKeys: signedFallbackKeys, - ) - : client.uploadKeysForDevice( - ourDeviceId!, - deviceKeys: uploadDeviceKeys - ? MatrixDeviceKeys.fromJson(deviceKeys) - : null, - oneTimeKeys: signedOneTimeKeys, - fallbackKeys: signedFallbackKeys, - )); + + if (ourDeviceId != client.deviceID) { + if (dehydratedDeviceAlgorithm == null || + dehydratedDevicePickleKey == null) { + throw Exception( + 'You need to provide both the pickle key and the algorithm to use dehydrated devices!'); + } + + await client.uploadDehydratedDevice( + deviceId: ourDeviceId!, + initialDeviceDisplayName: 'Dehydrated Device', + deviceKeys: + uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null, + oneTimeKeys: signedOneTimeKeys, + fallbackKeys: signedFallbackKeys, + deviceData: { + 'algorithm': dehydratedDeviceAlgorithm, + 'device': encryption.olmManager + .pickleOlmAccountWithKey(dehydratedDevicePickleKey), + }, + ); + return true; + } + final currentUpload = + this.currentUpload = CancelableOperation.fromFuture(client.uploadKeys( + deviceKeys: + uploadDeviceKeys ? MatrixDeviceKeys.fromJson(deviceKeys) : null, + oneTimeKeys: signedOneTimeKeys, + fallbackKeys: signedFallbackKeys, + )); final response = await currentUpload.valueOrCancellation(); if (response == null) { _uploadKeysLock = false; @@ -276,8 +289,8 @@ class OlmManager { // we failed to upload the keys. If we only tried to upload one time keys, try to recover by removing them and generating new ones. if (!uploadDeviceKeys && unusedFallbackKey != false && - !skipAllUploads && retry > 0 && + dehydratedDeviceAlgorithm != null && signedOneTimeKeys.isNotEmpty && exception.error == MatrixError.M_UNKNOWN) { Logs().w('Rotating otks because upload failed', exception); @@ -302,7 +315,6 @@ class OlmManager { oldKeyCount: oldKeyCount, updateDatabase: updateDatabase, unusedFallbackKey: unusedFallbackKey, - skipAllUploads: skipAllUploads, retry: retry - 1); } } finally { diff --git a/lib/msc_extensions/msc_3814_dehydrated_devices/api.dart b/lib/msc_extensions/msc_3814_dehydrated_devices/api.dart index 1f4b262aa..a36bada1b 100644 --- a/lib/msc_extensions/msc_3814_dehydrated_devices/api.dart +++ b/lib/msc_extensions/msc_3814_dehydrated_devices/api.dart @@ -29,41 +29,29 @@ import 'package:matrix/msc_extensions/msc_3814_dehydrated_devices/model/dehydrat /// Endpoints related to MSC3814, dehydrated devices v2 aka shrivelled sessions /// https://github.com/matrix-org/matrix-spec-proposals/pull/3814 extension DehydratedDeviceMatrixApi on MatrixApi { - /// Publishes end-to-end encryption keys for the specified device. - /// https://github.com/matrix-org/matrix-spec-proposals/pull/3814 - Future> uploadKeysForDevice(String device, - {MatrixDeviceKeys? deviceKeys, - Map? oneTimeKeys, - Map? fallbackKeys}) async { - final response = await request( - RequestType.POST, - '/client/v3/keys/upload/${Uri.encodeComponent(device)}', - data: { - if (deviceKeys != null) 'device_keys': deviceKeys.toJson(), - if (oneTimeKeys != null) 'one_time_keys': oneTimeKeys, - if (fallbackKeys != null) ...{ - 'fallback_keys': fallbackKeys, - 'org.matrix.msc2732.fallback_keys': fallbackKeys, - }, - }, - ); - return Map.from( - response.tryGetMap('one_time_key_counts') ?? - {}); - } - /// uploads a dehydrated device. /// https://github.com/matrix-org/matrix-spec-proposals/pull/3814 - Future uploadDehydratedDevice( - {String? initialDeviceDisplayName, - Map? deviceData}) async { + Future uploadDehydratedDevice({ + required String deviceId, + String? initialDeviceDisplayName, + Map? deviceData, + MatrixDeviceKeys? deviceKeys, + Map? oneTimeKeys, + Map? fallbackKeys, + }) async { final response = await request( RequestType.PUT, '/client/unstable/org.matrix.msc3814.v1/dehydrated_device', data: { + 'device_id': deviceId, if (initialDeviceDisplayName != null) 'initial_device_display_name': initialDeviceDisplayName, if (deviceData != null) 'device_data': deviceData, + if (deviceKeys != null) 'device_keys': deviceKeys.toJson(), + if (oneTimeKeys != null) 'one_time_keys': oneTimeKeys, + if (fallbackKeys != null) ...{ + 'fallback_keys': fallbackKeys, + }, }, ); return response['device_id'] as String; @@ -82,15 +70,15 @@ extension DehydratedDeviceMatrixApi on MatrixApi { /// fetch events sent to a dehydrated device. /// https://github.com/matrix-org/matrix-spec-proposals/pull/3814 Future getDehydratedDeviceEvents(String deviceId, - {String? from, int limit = 100}) async { - final response = await request( - RequestType.GET, - '/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events', - query: { - if (from != null) 'from': from, - 'limit': limit.toString(), - }, - ); + {String? nextBatch, int limit = 100}) async { + final response = await request(RequestType.POST, + '/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events', + query: { + 'limit': limit.toString(), + }, + data: { + if (nextBatch != null) 'next_batch': nextBatch, + }); return DehydratedDeviceEvents.fromJson(response); } } diff --git a/lib/msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart b/lib/msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart index 0098924a3..67e5d81f8 100644 --- a/lib/msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart +++ b/lib/msc_extensions/msc_3814_dehydrated_devices/msc_3814_dehydrated_devices.dart @@ -1,6 +1,7 @@ library msc_3814_dehydrated_devices; import 'dart:convert'; +import 'dart:math'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; @@ -78,10 +79,12 @@ extension DehydratedDeviceHandler on Client { // We need to be careful to not use the client.deviceId here and such. final encryption = Encryption(client: this); try { - await encryption.init(pickledDevice, - deviceId: device.deviceId, - pickleKey: pickleDeviceKey, - isDehydratedDevice: true); + await encryption.init( + pickledDevice, + deviceId: device.deviceId, + pickleKey: pickleDeviceKey, + dehydratedDeviceAlgorithm: _dehydratedDeviceAlgorithm, + ); if (dehydratedDeviceIdentity.curve25519Key != encryption.identityKey || dehydratedDeviceIdentity.ed25519Key != encryption.fingerprintKey) { @@ -97,7 +100,7 @@ extension DehydratedDeviceHandler on Client { do { events = await getDehydratedDeviceEvents(device.deviceId, - from: events?.nextBatch); + nextBatch: events?.nextBatch); for (final e in events.events ?? []) { // We are only interested in roomkeys, which ALWAYS need to be encrypted. @@ -111,6 +114,12 @@ extension DehydratedDeviceHandler on Client { } } while (events.events?.isNotEmpty == true); + // make sure the sessions we just received get uploaded before we upload a new device (which deletes the old device). + await this + .encryption + ?.keyManager + .uploadInboundGroupSessions(skipIfInProgress: false); + await _uploadNewDevice(secureStorage); } finally { await encryption.dispose(); @@ -136,18 +145,22 @@ extension DehydratedDeviceHandler on Client { _ssssSecretNameForDehydratedDevice, pickleDeviceKey); } + const chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + final rnd = Random(); + + final deviceIdSuffix = String.fromCharCodes(Iterable.generate( + 10, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); + final String device = 'FAM$deviceIdSuffix'; + // Generate a new olm account for the dehydrated device. - await encryption.init(null, - deviceId: null, isDehydratedDevice: true, pickleKey: pickleDeviceKey); - String device; try { - device = await uploadDehydratedDevice( - initialDeviceDisplayName: 'Dehydrated Device', - deviceData: { - 'algorithm': _dehydratedDeviceAlgorithm, - 'device': encryption.olmManager - .pickleOlmAccountWithKey(pickleDeviceKey), - }); + await encryption.init( + null, + deviceId: device, + pickleKey: pickleDeviceKey, + dehydratedDeviceAlgorithm: _dehydratedDeviceAlgorithm, + ); } on MatrixException catch (_) { // dehydrated devices unsupported, do noting. Logs().i('Dehydrated devices unsupported, skipping upload.'); @@ -158,11 +171,6 @@ extension DehydratedDeviceHandler on Client { encryption.ourDeviceId = device; encryption.olmManager.ourDeviceId = device; - await encryption.olmManager.uploadKeys( - uploadDeviceKeys: true, - updateDatabase: false, - unusedFallbackKey: true); - // cross sign the device from our currently signed in device await updateUserDeviceKeys(additionalUsers: {userID!}); final keysToSign = [ diff --git a/test/fake_matrix_api.dart b/test/fake_matrix_api.dart index c48ababe8..0af467495 100644 --- a/test/fake_matrix_api.dart +++ b/test/fake_matrix_api.dart @@ -207,7 +207,10 @@ class FakeMatrixApi extends BaseClient { } res = {}; } else { - res = {'errcode': 'M_UNRECOGNIZED', 'error': 'Unrecognized request'}; + res = { + 'errcode': 'M_UNRECOGNIZED', + 'error': 'Unrecognized request: $action' + }; statusCode = 405; } @@ -1978,29 +1981,6 @@ class FakeMatrixApi extends BaseClient { 'device_id': 'DEHYDDEV', 'device_data': {'algorithm': 'some.famedly.proprietary.algorithm'}, }, - '/client/unstable/org.matrix.msc3814.v1/dehydrated_device/DEHYDDEV/events?limit=100': - (var _) => { - 'events': [ - { - // this is the commented out m.room_key event - only encrypted - 'sender': '@othertest:fakeServer.notExisting', - 'content': { - 'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, - 'sender_key': - 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', - 'ciphertext': { - '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': { - 'type': 0, - 'body': - 'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', - }, - }, - }, - 'type': 'm.room.encrypted', - }, - ], - 'next_batch': 'd1', - }, }, 'POST': { '/client/v3/delete_devices': (var req) => {}, @@ -2409,6 +2389,29 @@ class FakeMatrixApi extends BaseClient { '/client/v3/rooms/!localpart%3Aserver.abc/invite': (var reqI) => {}, '/client/v3/keys/signatures/upload': (var reqI) => {'failures': {}}, '/client/v3/room_keys/version': (var reqI) => {'version': '5'}, + '/client/unstable/org.matrix.msc3814.v1/dehydrated_device/DEHYDDEV/events?limit=100': + (var _) => { + 'events': [ + { + // this is the commented out m.room_key event - only encrypted + 'sender': '@othertest:fakeServer.notExisting', + 'content': { + 'algorithm': AlgorithmTypes.olmV1Curve25519AesSha2, + 'sender_key': + 'JBG7ZaPn54OBC7TuIEiylW3BZ+7WcGQhFBPB9pogbAg', + 'ciphertext': { + '7rvl3jORJkBiK4XX1e5TnGnqz068XfYJ0W++Ml63rgk': { + 'type': 0, + 'body': + 'Awogyh7K4iLUQjcOxIfi7q7LhBBqv9w0mQ6JI9+U9tv7iF4SIHC6xb5YFWf9voRnmDBbd+0vxD/xDlVNRDlPIKliLGkYGiAkEbtlo+fng4ELtO4gSLKVbcFn7tZwZCEUE8H2miBsCCKABgMKIFrKDJwB7gM3lXPt9yVoh6gQksafKt7VFCNRN5KLKqsDEAAi0AX5EfTV7jJ1ZWAbxftjoSN6kCVIxzGclbyg1HjchmNCX7nxNCHWl+q5ZgqHYZVu2n2mCVmIaKD0kvoEZeY3tV1Itb6zf67BLaU0qgW/QzHCHg5a44tNLjucvL2mumHjIG8k0BY2uh+52HeiMCvSOvtDwHg7nzCASGdqPVCj9Kzw6z7F6nL4e3mYim8zvJd7f+mD9z3ARrypUOLGkTGYbB2PQOovf0Do8WzcaRzfaUCnuu/YVZWKK7DPgG8uhw/TjR6XtraAKZysF+4DJYMG9SQWx558r6s7Z5EUOF5CU2M35w1t1Xxllb3vrS83dtf9LPCrBhLsEBeYEUBE2+bTBfl0BDKqLiB0Cc0N0ixOcHIt6e40wAvW622/gMgHlpNSx8xG12u0s6h6EMWdCXXLWd9fy2q6glFUHvA67A35q7O+M8DVml7Y9xG55Y3DHkMDc9cwgwFkBDCAYQe6pQF1nlKytcVCGREpBs/gq69gHAStMQ8WEg38Lf8u8eBr2DFexrN4U+QAk+S//P3fJgf0bQx/Eosx4fvWSz9En41iC+ADCsWQpMbwHn4JWvtAbn3oW0XmL/OgThTkJMLiCymduYAa1Hnt7a3tP0KTL2/x11F02ggQHL28cCjq5W4zUGjWjl5wo2PsKB6t8aAvMg2ujGD2rCjb4yrv5VIzAKMOZLyj7K0vSK9gwDLQ/4vq+QnKUBG5zrcOze0hX+kz2909/tmAdeCH61Ypw7gbPUJAKnmKYUiB/UgwkJvzMJSsk/SEs5SXosHDI+HsJHJp4Mp4iKD0xRMst+8f9aTjaWwh8ZvELE1ZOhhCbF3RXhxi3x2Nu8ORIz+vhEQ1NOlMc7UIo98Fk/96T36vL/fviowT4C/0AlaapZDJBmKwhmwqisMjY2n1vY29oM2p5BzY1iwP7q9BYdRFst6xwo57TNSuRwQw7IhFsf0k+ABuPEZy5xB5nPHyIRTf/pr3Hw', + }, + }, + }, + 'type': 'm.room.encrypted', + }, + ], + 'next_batch': 'd1', + }, }, 'PUT': { '/client/v3/user/${Uri.encodeComponent('@alice:example.com')}/account_data/io.element.recent_emoji}': diff --git a/test/msc_extensions/msc_3814_dehydrated_devices_test.dart b/test/msc_extensions/msc_3814_dehydrated_devices_test.dart index 903f360a8..fcc681b43 100644 --- a/test/msc_extensions/msc_3814_dehydrated_devices_test.dart +++ b/test/msc_extensions/msc_3814_dehydrated_devices_test.dart @@ -34,6 +34,7 @@ void main() { final client = await getClient(); final ret = await client.uploadDehydratedDevice( + deviceId: 'DEHYDDEV', initialDeviceDisplayName: 'DehydratedDevice', deviceData: {'algorithm': 'some.famedly.proprietary.algorith'}); expect(