Merge pull request #1614 from famedly/nico/dehydrated-devices-v2
feat: Update dehydrated devices implementation to current MSC
This commit is contained in:
commit
d35872b4ad
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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 {
|
||||
|
|
@ -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 = <String, dynamic>{
|
||||
'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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
'/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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = <SignableKey>[
|
||||
|
|
|
|||
|
|
@ -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}':
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue