dehydratedDeviceSetup method
Restores the dehydrated device account and/or creates a new one, fetches the events and as such makes encrypted messages available while we were offline. Usually it only makes sense to call this when you just entered the SSSS passphrase or recovery key successfully.
Implementation
Future<void> dehydratedDeviceSetup(OpenSSSS secureStorage) async {
try {
// dehydrated devices need to be cross-signed
if (!enableDehydratedDevices ||
!encryptionEnabled ||
this.encryption?.crossSigning.enabled != true) {
return;
}
DehydratedDevice? device;
try {
device = await getDehydratedDevice();
} on SDNException catch (e) {
if (e.response?.statusCode == 400) {
Logs().i('Dehydrated devices unsupported, skipping.');
return;
}
// No device, so we just create a new device.
await _uploadNewDevice(secureStorage);
return;
}
// Just throw away the old device if it is using an old algoritm. In the future we could try to still use it and then migrate it, but currently that is not worth the effort
if (_oldDehydratedDeviceAlgorithms
.contains(device.deviceData?.tryGet<String>('algorithm'))) {
await _uploadNewDevice(secureStorage);
return;
}
// Only handle devices we understand
// In the future we might want to migrate to a newer format here
if (device.deviceData?.tryGet<String>('algorithm') !=
_dehydratedDeviceAlgorithm) return;
// Verify that the device is cross-signed
final dehydratedDeviceIdentity =
userDeviceKeys[userID]!.deviceKeys[device.deviceId];
if (dehydratedDeviceIdentity == null ||
!dehydratedDeviceIdentity.hasValidSignatureChain()) {
Logs().w(
'Dehydrated device ${device.deviceId} is unknown or unverified, replacing it');
await _uploadNewDevice(secureStorage);
return;
}
final pickleDeviceKey =
await secureStorage.getStored(_ssssSecretNameForDehydratedDevice);
final pickledDevice = device.deviceData?.tryGet<String>('device');
if (pickledDevice == null) {
Logs()
.w('Dehydrated device ${device.deviceId} is invalid, replacing it');
await _uploadNewDevice(secureStorage);
return;
}
// Use a separate encryption object for the dehydrated device.
// 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);
if (dehydratedDeviceIdentity.curve25519Key != encryption.identityKey ||
dehydratedDeviceIdentity.ed25519Key != encryption.fingerprintKey) {
Logs()
.w('Invalid dehydrated device ${device.deviceId}, replacing it');
await encryption.dispose();
await _uploadNewDevice(secureStorage);
return;
}
// Fetch the to_device messages sent to the picked device and handle them 1:1.
DehydratedDeviceEvents? events;
do {
events = await getDehydratedDeviceEvents(device.deviceId,
from: events?.nextBatch);
for (final e in events.events ?? []) {
// We are only interested in roomkeys, which ALWAYS need to be encrypted.
if (e.type == EventTypes.Encrypted) {
final decryptedEvent = await encryption.decryptToDeviceEvent(e);
if (decryptedEvent.type == EventTypes.RoomKey) {
await encryption.handleToDeviceEvent(decryptedEvent);
}
}
}
} while (events.events?.isNotEmpty == true);
await _uploadNewDevice(secureStorage);
} finally {
await encryption.dispose();
}
} catch (e) {
Logs().w('Exception while handling dehydrated devices: ${e.toString()}');
return;
}
}