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 = 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;
}