uploadKeys method

Future<bool> uploadKeys({
  1. bool uploadDeviceKeys = false,
  2. int? oldKeyCount = 0,
  3. bool updateDatabase = true,
  4. bool? unusedFallbackKey = false,
  5. String? dehydratedDeviceAlgorithm,
  6. String? dehydratedDevicePickleKey,
  7. int retry = 1,
})

Generates new one time keys, signs everything and upload it to the server. If retry is > 0, the request will be retried with new OTKs on upload failure.

Implementation

Future<bool> uploadKeys({
  bool uploadDeviceKeys = false,
  int? oldKeyCount = 0,
  bool updateDatabase = true,
  bool? unusedFallbackKey = false,
  String? dehydratedDeviceAlgorithm,
  String? dehydratedDevicePickleKey,
  int retry = 1,
}) async {
  final olmAccount = _olmAccount;
  if (olmAccount == null) {
    return true;
  }

  if (_uploadKeysLock) {
    return false;
  }
  _uploadKeysLock = true;

  final signedOneTimeKeys = <String, Map<String, Object?>>{};
  try {
    int? uploadedOneTimeKeysCount;
    if (oldKeyCount != null) {
      // check if we have OTKs that still need uploading. If we do, we don't try to generate new ones,
      // instead we try to upload the old ones first
      final oldOTKsNeedingUpload = olmAccount.oneTimeKeys.length;

      // generate one-time keys
      // we generate 2/3rds of max, so that other keys people may still have can
      // still be used
      final oneTimeKeysCount =
          (olmAccount.maxNumberOfOneTimeKeys * 2 / 3).floor() -
              oldKeyCount -
              oldOTKsNeedingUpload;
      if (oneTimeKeysCount > 0) {
        olmAccount.generateOneTimeKeys(oneTimeKeysCount);
      }
      uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload;
    }

    if (unusedFallbackKey == false) {
      // we don't have an unused fallback key uploaded....so let's change that!
      olmAccount.generateFallbackKey();
    }

    // we save the generated OTKs into the database.
    // in case the app gets killed during upload or the upload fails due to bad network
    // we can still re-try later
    if (updateDatabase) {
      await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!);
    }

    // and now generate the payload to upload
    var deviceKeys = <String, dynamic>{
      'user_id': client.userID,
      'device_id': ourDeviceId,
      'algorithms': [
        AlgorithmTypes.olmV1Curve25519AesSha2,
        AlgorithmTypes.megolmV1AesSha2,
      ],
      'keys': <String, dynamic>{},
    };

    if (uploadDeviceKeys) {
      final keys = olmAccount.identityKeys;
      deviceKeys['keys']['curve25519:$ourDeviceId'] =
          keys.curve25519.toBase64();
      deviceKeys['keys']['ed25519:$ourDeviceId'] = keys.ed25519.toBase64();
      deviceKeys = signJson(deviceKeys);
    }

    // now sign all the one-time keys
    for (final entry in olmAccount.oneTimeKeys.entries) {
      final key = entry.key;
      final value = entry.value.toBase64();
      signedOneTimeKeys['signed_curve25519:$key'] = signJson({
        'key': value,
      });
    }

    final signedFallbackKeys = <String, dynamic>{};
    final fallbackKey = olmAccount.fallbackKey;
    // now sign all the fallback keys
    for (final entry in fallbackKey.entries) {
      final key = entry.key;
      final value = entry.value.toBase64();
      signedFallbackKeys['signed_curve25519:$key'] = signJson({
        'key': value,
        'fallback': true,
      });
    }

    if (signedFallbackKeys.isEmpty &&
        signedOneTimeKeys.isEmpty &&
        !uploadDeviceKeys) {
      _uploadKeysLock = false;
      return true;
    }

    // Workaround: Make sure we stop if we got logged out in the meantime.
    if (!client.isLogged()) return true;

    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: client.dehydratedDeviceDisplayName,
        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;
      return false;
    }

    // mark the OTKs as published and save that to datbase
    olmAccount.markKeysAsPublished();
    if (updateDatabase) {
      await encryption.olmDatabase?.updateClientKeys(pickledOlmAccount!);
    }
    return (uploadedOneTimeKeysCount != null &&
            response['signed_curve25519'] == uploadedOneTimeKeysCount) ||
        uploadedOneTimeKeysCount == null;
  } on MatrixException catch (exception) {
    _uploadKeysLock = false;

    // 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 &&
        retry > 0 &&
        dehydratedDeviceAlgorithm != null &&
        signedOneTimeKeys.isNotEmpty &&
        exception.error == MatrixError.M_UNKNOWN) {
      Logs().w('Rotating otks because upload failed', exception);
      for (final otk in signedOneTimeKeys.values) {
        final key = otk.tryGet<String>('key');
        if (key != null) {
          olmAccount.removeOneTimeKey(key);
        }
      }

      await uploadKeys(
        uploadDeviceKeys: uploadDeviceKeys,
        oldKeyCount: oldKeyCount,
        updateDatabase: updateDatabase,
        unusedFallbackKey: unusedFallbackKey,
        retry: retry - 1,
      );
    }
  } finally {
    _uploadKeysLock = false;
  }

  return false;
}