feat: Update dehydrated devices implementation to current MSC

BREAKING CHANGE: This replaces the old dehydrated devices
implementation, since there is no way to query what is supported easily
and supporting both would be complicated.

fixes https://github.com/famedly/matrix-dart-sdk/issues/1579
This commit is contained in:
Nicolas Werner 2023-11-17 00:03:49 +01:00
parent 4f9581709b
commit 2fca08725d
No known key found for this signature in database
GPG Key ID: B38119FF80087618
6 changed files with 130 additions and 119 deletions

View File

@ -68,15 +68,20 @@ class Encryption {
}
// initial login passes null to init a new olm account
Future<void> init(String? olmAccount,
{String? deviceId,
Future<void> init(
String? olmAccount, {
String? deviceId,
String? pickleKey,
bool isDehydratedDevice = false}) async {
String? dehydratedDeviceAlgorithm,
}) async {
ourDeviceId = deviceId ?? client.deviceID!;
final isDehydratedDevice = dehydratedDeviceAlgorithm != null;
await olmManager.init(
olmAccount: olmAccount,
deviceId: isDehydratedDevice ? deviceId : ourDeviceId,
pickleKey: pickleKey);
pickleKey: pickleKey,
dehydratedDeviceAlgorithm: dehydratedDeviceAlgorithm,
);
if (!isDehydratedDevice) keyManager.startAutoUploadKeys();
}

View File

@ -57,10 +57,12 @@ class OlmManager {
final Map<String, List<OlmSession>> _olmSessions = {};
// NOTE(Nico): On initial login we pass null to create a new account
Future<void> init(
{String? olmAccount,
Future<void> init({
String? olmAccount,
required String? deviceId,
String? pickleKey}) async {
String? pickleKey,
String? dehydratedDeviceAlgorithm,
}) async {
ourDeviceId = deviceId;
if (olmAccount == null) {
try {
@ -70,8 +72,10 @@ class OlmManager {
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)) {
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 = <String, dynamic>{
'user_id': client.userID,
@ -239,20 +239,33 @@ 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,
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,
)
: client.uploadKeysForDevice(
ourDeviceId!,
deviceKeys: uploadDeviceKeys
? MatrixDeviceKeys.fromJson(deviceKeys)
: null,
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,
));
@ -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 {

View File

@ -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<Map<String, int>> uploadKeysForDevice(String device,
{MatrixDeviceKeys? deviceKeys,
Map<String, dynamic>? oneTimeKeys,
Map<String, dynamic>? 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<String, int>.from(
response.tryGetMap<String, Object?>('one_time_key_counts') ??
<String, int>{});
}
/// uploads a dehydrated device.
/// https://github.com/matrix-org/matrix-spec-proposals/pull/3814
Future<String> uploadDehydratedDevice(
{String? initialDeviceDisplayName,
Map<String, dynamic>? deviceData}) async {
Future<String> uploadDehydratedDevice({
required String deviceId,
String? initialDeviceDisplayName,
Map<String, dynamic>? deviceData,
MatrixDeviceKeys? deviceKeys,
Map<String, dynamic>? oneTimeKeys,
Map<String, dynamic>? 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<DehydratedDeviceEvents> getDehydratedDeviceEvents(String deviceId,
{String? from, int limit = 100}) async {
final response = await request(
RequestType.GET,
{String? nextBatch, int limit = 100}) async {
final response = await request(RequestType.POST,
'/client/unstable/org.matrix.msc3814.v1/dehydrated_device/$deviceId/events',
query: {
if (from != null) 'from': from,
'limit': limit.toString(),
},
);
data: {
if (nextBatch != null) 'next_batch': nextBatch,
});
return DehydratedDeviceEvents.fromJson(response);
}
}

View File

@ -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,
await encryption.init(
pickledDevice,
deviceId: device.deviceId,
pickleKey: pickleDeviceKey,
isDehydratedDevice: true);
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.
@ -136,18 +139,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 +165,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 = <SignableKey>[

View File

@ -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}':

View File

@ -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(