uploadKeys method

Future<bool> uploadKeys({
  1. bool uploadDeviceKeys = false,
  2. int? oldKeyCount = 0,
  3. bool updateDatabase = true,
  4. bool? unusedFallbackKey = false,
  5. bool skipAllUploads = false,
  6. 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,
  bool skipAllUploads = false,
  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!);
    }

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

    // 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;
    final currentUpload = this.currentUpload =
        CancelableOperation.fromFuture(ourDeviceId == client.deviceID
            ? client.uploadKeys(
                deviceKeys: uploadDeviceKeys
                    ? SDNDeviceKeys.fromJson(deviceKeys)
                    : null,
                oneTimeKeys: signedOneTimeKeys,
                fallbackKeys: signedFallbackKeys,
              )
            : client.uploadKeysForDevice(
                ourDeviceId!,
                deviceKeys: uploadDeviceKeys
                    ? SDNDeviceKeys.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 SDNException 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 &&
        !skipAllUploads &&
        retry > 0 &&
        signedOneTimeKeys.isNotEmpty &&
        exception.error == SDNError.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,
          skipAllUploads: skipAllUploads,
          retry: retry - 1);
    }
  } finally {
    _uploadKeysLock = false;
  }

  return false;
}