Skip to content

Commit

Permalink
feat: Update dehydrated devices implementation to current MSC
Browse files Browse the repository at this point in the history
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 #1579
  • Loading branch information
nico-famedly committed Nov 16, 2023
1 parent 76b216c commit 9f69fb9
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 119 deletions.
19 changes: 12 additions & 7 deletions lib/encryption/encryption.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,20 @@ class Encryption {
}

// initial login passes null to init a new olm account
Future<void> init(String? olmAccount,
{String? deviceId,
String? pickleKey,
bool isDehydratedDevice = false}) async {
Future<void> 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();
}
Expand Down
78 changes: 45 additions & 33 deletions lib/encryption/olm_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,25 @@ 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,
required String? deviceId,
String? pickleKey}) async {
Future<void> init({
String? olmAccount,
required String? deviceId,
String? pickleKey,
String? dehydratedDeviceAlgorithm,
}) async {
ourDeviceId = deviceId;
if (olmAccount == null) {
try {
await olm.init();
_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 (_) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -302,7 +315,6 @@ class OlmManager {
oldKeyCount: oldKeyCount,
updateDatabase: updateDatabase,
unusedFallbackKey: unusedFallbackKey,
skipAllUploads: skipAllUploads,
retry: retry - 1);
}
} finally {
Expand Down
58 changes: 23 additions & 35 deletions lib/msc_extensions/msc_3814_dehydrated_devices/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
'/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);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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.');
Expand All @@ -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>[
Expand Down
51 changes: 27 additions & 24 deletions test/fake_matrix_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -1977,29 +1980,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) => {},
Expand Down Expand Up @@ -2408,6 +2388,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}':
Expand Down
1 change: 1 addition & 0 deletions test/msc_extensions/msc_3814_dehydrated_devices_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 9f69fb9

Please sign in to comment.