From 4ac90f10c163223d75ae135240f7fb69cdc082cc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 7 Oct 2021 12:10:41 +0200 Subject: [PATCH] Fallback keys implementation. Author: Onuray - Benoit squashes the 4 commit to cancel the addition on binaries --- .../api/session/sync/model/SyncResponse.kt | 8 ++++ .../internal/crypto/DefaultCryptoService.kt | 17 ++++++-- .../sdk/internal/crypto/MXOlmDevice.kt | 18 ++++++++ .../internal/crypto/OneTimeKeysUploader.kt | 41 +++++++++++++++---- .../crypto/model/rest/KeysUploadBody.kt | 9 +++- .../internal/crypto/tasks/UploadKeysTask.kt | 6 ++- 6 files changed, 84 insertions(+), 15 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt index 876e99da63..d7dff72288 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt @@ -58,6 +58,14 @@ data class SyncResponse( @Json(name = "device_one_time_keys_count") val deviceOneTimeKeysCount: DeviceOneTimeKeysCountSyncResponse? = null, + /** + * The key algorithms for which the server has an unused fallback key for the device. + * If the client wants the server to have a fallback key for a given key algorithm, + * but that algorithm is not listed in device_unused_fallback_key_types, the client will upload a new key. + */ + @Json(name = "org.matrix.msc2732.device_unused_fallback_key_types") + val deviceUnusedFallbackKeyTypes: List? = null, + /** * List of groups. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 18f344882c..7e7d0e7f58 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -67,6 +67,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent @@ -329,7 +330,7 @@ internal class DefaultCryptoService @Inject constructor( uploadDeviceKeys() } - oneTimeKeysUploader.maybeUploadOneTimeKeys() + oneTimeKeysUploader.maybeUploadOneTimeKeys(shouldGenerateFallbackKey = false) // this can throw if no backup tryOrNull { keysBackupService.checkAndStartKeysBackup() @@ -431,7 +432,17 @@ internal class DefaultCryptoService @Inject constructor( if (isStarted()) { // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() - oneTimeKeysUploader.maybeUploadOneTimeKeys() + // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys. + // If there's no unused signed_curve25519 fallback key we need a new one. + val shouldGenerateFallbackKey = if (syncResponse.deviceUnusedFallbackKeyTypes != null) { + // Generate a fallback key only if the server does not already have an unused fallback key. + !syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE) + } else { + // Server does not support fallbackKey + false + } + + oneTimeKeysUploader.maybeUploadOneTimeKeys(shouldGenerateFallbackKey) incomingGossipingRequestManager.processReceivedGossipingRequests() } } @@ -928,7 +939,7 @@ internal class DefaultCryptoService @Inject constructor( signatures = objectSigner.signObject(canonicalJson) ) - val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null) + val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null, null) uploadKeysTask.execute(uploadDeviceKeysParams) cryptoStore.setDeviceKeysUploaded(true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 441dfe4a5d..6479a8ddce 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -136,6 +136,24 @@ internal class MXOlmDevice @Inject constructor( return store.getOlmAccount().maxOneTimeKeys() } + fun getFallbackKey(): MutableMap>? { + try { + return store.getOlmAccount().fallbackKey() + } catch (e: Exception) { + Timber.e("## getFallbackKey() : failed") + } + return null + } + + fun generateFallbackKey() { + try { + store.getOlmAccount().generateFallbackKey() + store.saveOlmAccount() + } catch (e: Exception) { + Timber.e("## generateFallbackKey() : failed") + } + } + /** * Release the instance */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt index c4b62fe9fe..5bf047bf4e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -54,7 +54,7 @@ internal class OneTimeKeysUploader @Inject constructor( /** * Check if the OTK must be uploaded. */ - suspend fun maybeUploadOneTimeKeys() { + suspend fun maybeUploadOneTimeKeys(shouldGenerateFallbackKey: Boolean) { if (oneTimeKeyCheckInProgress) { Timber.v("maybeUploadOneTimeKeys: already in progress") return @@ -68,6 +68,10 @@ internal class OneTimeKeysUploader @Inject constructor( lastOneTimeKeyCheck = System.currentTimeMillis() oneTimeKeyCheckInProgress = true + if (shouldGenerateFallbackKey) { + olmDevice.generateFallbackKey() + } + // We then check how many keys we can store in the Account object. val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys() @@ -96,7 +100,7 @@ internal class OneTimeKeysUploader @Inject constructor( // So we need some kind of engineering compromise to balance all of // these factors. tryOrNull("Unable to upload OTK") { - val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit) + val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit, shouldGenerateFallbackKey) Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent") } } else { @@ -108,7 +112,7 @@ internal class OneTimeKeysUploader @Inject constructor( private suspend fun fetchOtkCount(): Int? { return tryOrNull("Unable to get OTK count") { - val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null)) + val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null, null)) result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) } } @@ -120,19 +124,22 @@ internal class OneTimeKeysUploader @Inject constructor( * @param keyLimit the limit * @return the number of uploaded keys */ - private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int { - if (keyLimit <= keyCount) { + private suspend fun uploadOTK(keyCount: Int, keyLimit: Int, shouldGenerateFallbackKey: Boolean): Int { + if (keyLimit <= keyCount && !shouldGenerateFallbackKey) { // If we don't need to generate any more keys then we are done. return 0 } val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) olmDevice.generateOneTimeKeys(keysThisLoop) - val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys()) + + val fallbackKey = if (shouldGenerateFallbackKey) olmDevice.getFallbackKey() else null + + val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys(), fallbackKey) olmDevice.markKeysAsPublished() if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { // Maybe upload other keys - return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) + return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit, false) } else { Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") @@ -142,7 +149,7 @@ internal class OneTimeKeysUploader @Inject constructor( /** * Upload curve25519 one time keys. */ - private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?): KeysUploadResponse { + private suspend fun uploadOneTimeKeys(oneTimeKeys: Map>?, fallbackKey: Map>?): KeysUploadResponse { val oneTimeJson = mutableMapOf() val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() @@ -159,9 +166,25 @@ internal class OneTimeKeysUploader @Inject constructor( oneTimeJson["signed_curve25519:$key_id"] = k } + val fallbackJson = mutableMapOf() + val fallbackCurve25519Map = fallbackKey?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty() + fallbackCurve25519Map.forEach { (key_id, key) -> + val k = mutableMapOf() + k["key"] = key + k["fallback"] = true + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k) + k["signatures"] = objectSigner.signObject(canonicalJson) + + fallbackJson["signed_curve25519:$key_id"] = k + } + // For now, we set the device id explicitly, as we may not be using the // same one as used in login. - val uploadParams = UploadKeysTask.Params(null, oneTimeJson) + val uploadParams = UploadKeysTask.Params( + deviceKeys = null, + oneTimeKeys = oneTimeJson, + fallbackKeys = if (fallbackJson.isNotEmpty()) fallbackJson else null + ) return uploadKeysTask.execute(uploadParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt index 69b3992374..363dee9a8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt @@ -40,5 +40,12 @@ internal data class KeysUploadBody( * May be absent if no new one-time keys are required. */ @Json(name = "one_time_keys") - val oneTimeKeys: JsonDict? = null + val oneTimeKeys: JsonDict? = null, + + /** + * If the user had previously uploaded a fallback key for a given algorithm, it is replaced. + * The server will only keep one fallback key per algorithm for each user. + */ + @Json(name = "org.matrix.msc2732.fallback_keys") + val fallbackKeys: JsonDict? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt index cac4dadd93..30de8e871a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadKeysTask.kt @@ -32,7 +32,8 @@ internal interface UploadKeysTask : Task $body")