dehydratedDeviceSetup method

Future<void> dehydratedDeviceSetup(
  1. OpenSSSS secureStorage
)

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 MatrixException 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,
        dehydratedDeviceAlgorithm: _dehydratedDeviceAlgorithm,
      );

      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,
            nextBatch: 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);

      // 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();
    }
  } catch (e) {
    Logs().w('Exception while handling dehydrated devices: ${e.toString()}');
    return;
  }
}