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 = json
          .decode(olmAccount.one_time_keys())['curve25519']
          .entries
          .length as int;
      // 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.max_number_of_one_time_keys() * 2 / 3).floor() -
              oldKeyCount -
              oldOTKsNeedingUpload;
      if (oneTimeKeysCount > 0) {
        olmAccount.generate_one_time_keys(oneTimeKeysCount);
      }
      uploadedOneTimeKeysCount = oneTimeKeysCount + oldOTKsNeedingUpload;
    }

    if (encryption.isMinOlmVersion(3, 2, 7) && unusedFallbackKey == false) {
      // we don't have an unused fallback key uploaded....so let's change that!
      olmAccount.generate_fallback_key();
    }

    // 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 Map<String, dynamic> keys =
          json.decode(olmAccount.identity_keys());
      for (final entry in keys.entries) {
        final algorithm = entry.key;
        final value = entry.value;
        deviceKeys['keys']['$algorithm:$ourDeviceId'] = value;
      }
      deviceKeys = signJson(deviceKeys);
    }

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

    final signedFallbackKeys = <String, dynamic>{};
    if (encryption.isMinOlmVersion(3, 2, 7)) {
      final fallbackKey = json.decode(olmAccount.unpublished_fallback_key());
      // now sign all the fallback keys
      for (final entry in fallbackKey['curve25519'].entries) {
        final key = entry.key;
        final value = entry.value;
        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: '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;
      return false;
    }

    // mark the OTKs as published and save that to datbase
    olmAccount.mark_keys_as_published();
    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) {
        // Keys can only be removed by creating a session...
        final session = olm.Session();
        try {
          final String identity =
              json.decode(olmAccount.identity_keys())['curve25519'];
          final key = otk.tryGet<String>('key');
          if (key != null) {
            session.create_outbound(_olmAccount!, identity, key);
            olmAccount.remove_one_time_keys(session);
          }
        } finally {
          session.free();
        }
      }

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

  return false;
}