uploadKeys method
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;
}