Backup: refactor to extract everything related to specific algorithm
This commit is contained in:
parent
f855664504
commit
c055f40607
@ -25,8 +25,8 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
@ -35,8 +35,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NA
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
@ -179,8 +179,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
|
||||
return MegolmBackupAuthData(
|
||||
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
|
||||
assertEquals(EventType.ENCRYPTED, event.type)
|
||||
assertNotNull(event.content)
|
||||
|
||||
val eventWireContent = event.content.toContent()
|
||||
assertNotNull(eventWireContent)
|
||||
|
||||
assertNull(eventWireContent["body"])
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
|
||||
|
||||
assertNotNull(eventWireContent["ciphertext"])
|
||||
assertNotNull(eventWireContent["session_id"])
|
||||
assertNotNull(eventWireContent["sender_key"])
|
||||
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||
|
||||
assertNotNull(event.eventId)
|
||||
assertEquals(roomId, event.roomId)
|
||||
assertEquals(EventType.MESSAGE, event.getClearType())
|
||||
// TODO assertTrue(event.getAge() < 10000)
|
||||
|
||||
val eventContent = event.toContent()
|
||||
assertNotNull(eventContent)
|
||||
assertEquals(clearMessage, eventContent["body"])
|
||||
assertEquals(senderSession.myUserId, event.senderId)
|
||||
}
|
||||
|
||||
fun createFakeMegolmBackupAuthData(): MegolmBackupCurve25519AuthData {
|
||||
return MegolmBackupCurve25519AuthData(
|
||||
publicKey = "abcdefg",
|
||||
signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop"))
|
||||
)
|
||||
@ -288,6 +315,24 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||
secret,
|
||||
listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
// set up megolm backup
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||
}
|
||||
val version = awaitCallback<KeysVersion> {
|
||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||
}
|
||||
// Save it for gossiping
|
||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||
|
||||
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
ssssService.storeSecret(
|
||||
KEYBACKUP_SECRET_SSSS_NAME,
|
||||
secret,
|
||||
listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,7 +218,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
|
||||
val keysBackupService = aliceSession.cryptoService().keysBackupService()
|
||||
val keyBackupPassword = "FooBarBaz"
|
||||
val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
||||
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, null, it)
|
||||
}
|
||||
val version = commonTestHelper.waitForCallback<KeysVersion> {
|
||||
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||
|
@ -224,7 +224,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
|
||||
val keyBackupPassword = "FooBarBaz"
|
||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
||||
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, null, it)
|
||||
}
|
||||
val version = testHelper.waitForCallback<KeysVersion> {
|
||||
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||
|
@ -41,6 +41,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTru
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
@ -49,6 +50,10 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.waitFor
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.algorithm.KeysBackupAlgorithmFactory
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
|
||||
import java.security.InvalidParameterException
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@ -119,11 +124,12 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertFalse(keysBackup.isEnabled())
|
||||
|
||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||
keysBackup.prepareKeysBackupVersion(null, null, it)
|
||||
keysBackup.prepareKeysBackupVersion(null, null, null, it)
|
||||
}
|
||||
|
||||
assertEquals(MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP, megolmBackupCreationInfo.algorithm)
|
||||
assertNotNull(megolmBackupCreationInfo.authData.publicKey)
|
||||
val authData = megolmBackupCreationInfo.authData as MegolmBackupCurve25519AuthData
|
||||
assertNotNull(authData.publicKey)
|
||||
assertNotNull(megolmBackupCreationInfo.authData.signatures)
|
||||
assertNotNull(megolmBackupCreationInfo.recoveryKey)
|
||||
|
||||
@ -145,7 +151,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertFalse(keysBackup.isEnabled())
|
||||
|
||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||
keysBackup.prepareKeysBackupVersion(null, null, it)
|
||||
keysBackup.prepareKeysBackupVersion(null, null, null, it)
|
||||
}
|
||||
|
||||
assertFalse(keysBackup.isEnabled())
|
||||
@ -298,23 +304,25 @@ class KeysBackupTest : InstrumentedTest {
|
||||
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
|
||||
|
||||
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
||||
|
||||
val keysBackupVersion = keysBackup.keysBackupVersion
|
||||
assertNotNull(keysBackupVersion)
|
||||
val algorithm = KeysBackupAlgorithmFactory().create(keysBackupVersion!!)
|
||||
// - Check encryptGroupSession() returns stg
|
||||
val keyBackupData = keysBackup.encryptGroupSession(session)
|
||||
assertNotNull(keyBackupData)
|
||||
assertNotNull(keyBackupData!!.sessionData)
|
||||
|
||||
// - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
|
||||
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
|
||||
assertNotNull(decryption)
|
||||
// - Check decryptKeyBackupData() returns stg
|
||||
val sessionData = keysBackup
|
||||
.decryptKeyBackupData(
|
||||
keyBackupData,
|
||||
session.safeSessionId!!,
|
||||
cryptoTestData.roomId,
|
||||
decryption!!
|
||||
)
|
||||
val roomKeysBackupData = RoomKeysBackupData(
|
||||
HashMap<String, KeyBackupData>().apply {
|
||||
put(session.safeSessionId!!, keyBackupData)
|
||||
}
|
||||
)
|
||||
val keysBackupData = KeysBackupData(
|
||||
HashMap<String, RoomKeysBackupData>().apply {
|
||||
put(cryptoTestData.roomId, roomKeysBackupData)
|
||||
}
|
||||
)
|
||||
val sessionsData = algorithm.decryptSessions(keyBackupCreationInfo.recoveryKey, keysBackupData)
|
||||
val sessionData = sessionsData.firstOrNull()
|
||||
assertNotNull(sessionData)
|
||||
// - Compare the decrypted megolm key with the original one
|
||||
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
|
||||
|
@ -105,7 +105,7 @@ internal class KeysBackupTestHelper(
|
||||
val stateObserver = StateObserver(keysBackup)
|
||||
|
||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||
keysBackup.prepareKeysBackupVersion(password, null, it)
|
||||
keysBackup.prepareKeysBackupVersion(password, null, null, it)
|
||||
}
|
||||
|
||||
Assert.assertNotNull(megolmBackupCreationInfo)
|
||||
|
@ -124,6 +124,7 @@ interface KeysBackupService {
|
||||
*/
|
||||
fun prepareKeysBackupVersion(
|
||||
password: String?,
|
||||
algorithm: String? = null,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||
)
|
||||
|
@ -24,14 +24,16 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorith
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeysVersionResult(
|
||||
/**
|
||||
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
|
||||
* The algorithm used for storing backups.
|
||||
* Currently, "m.megolm_backup.v1.curve25519-aes-sha2" and
|
||||
* org.matrix.msc3270.v1.aes-hmac-sha2 are defined.
|
||||
*/
|
||||
@Json(name = "algorithm")
|
||||
override val algorithm: String,
|
||||
|
||||
/**
|
||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2".
|
||||
* @see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||
* @see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupCurve25519AuthData]
|
||||
*/
|
||||
@Json(name = "auth_data")
|
||||
override val authData: JsonDict,
|
||||
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Data model for [org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData.authData] in case
|
||||
* of [org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MegolmBackupAes256AuthData(
|
||||
|
||||
/**
|
||||
* The identity vector used to encrypt the backups.
|
||||
*/
|
||||
@Json(name = "iv")
|
||||
val iv: String? = null,
|
||||
|
||||
/**
|
||||
* The mac used to encrypt the backups.
|
||||
*/
|
||||
@Json(name = "mac")
|
||||
val mac: String? = null,
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the salt associated with the backup
|
||||
* private key.
|
||||
*/
|
||||
@Json(name = "private_key_salt")
|
||||
override val privateKeySalt: String? = null,
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the number of key derivations.
|
||||
*/
|
||||
@Json(name = "private_key_iterations")
|
||||
override val privateKeyIterations: Int? = null,
|
||||
|
||||
/**
|
||||
* Signatures of the public key.
|
||||
* userId -> (deviceSignKeyId -> signature)
|
||||
*/
|
||||
@Json(name = "signatures")
|
||||
override val signatures: Map<String, Map<String, String>>? = null
|
||||
|
||||
) : MegolmBackupAuthData {
|
||||
|
||||
override fun isValid(): Boolean = !(iv.isNullOrEmpty() || mac.isNullOrEmpty())
|
||||
|
||||
override fun copy(newSignatures: Map<String, Map<String, String>>?): MegolmBackupAuthData {
|
||||
return copy(signatures = newSignatures)
|
||||
}
|
||||
|
||||
override fun toJsonDict(): JsonDict {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val adapter = moshi.adapter(Map::class.java)
|
||||
|
||||
return moshi
|
||||
.adapter(MegolmBackupAes256AuthData::class.java)
|
||||
.toJson(this)
|
||||
.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
adapter.fromJson(it) as JsonDict
|
||||
}
|
||||
}
|
||||
}
|
@ -16,64 +16,15 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case
|
||||
* of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MegolmBackupAuthData(
|
||||
/**
|
||||
* The curve25519 public key used to encrypt the backups.
|
||||
*/
|
||||
@Json(name = "public_key")
|
||||
val publicKey: String,
|
||||
sealed interface MegolmBackupAuthData {
|
||||
val privateKeySalt: String?
|
||||
val privateKeyIterations: Int?
|
||||
val signatures: Map<String, Map<String, String>>?
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the salt associated with the backup
|
||||
* private key.
|
||||
*/
|
||||
@Json(name = "private_key_salt")
|
||||
val privateKeySalt: String? = null,
|
||||
fun isValid(): Boolean
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the number of key derivations.
|
||||
*/
|
||||
@Json(name = "private_key_iterations")
|
||||
val privateKeyIterations: Int? = null,
|
||||
|
||||
/**
|
||||
* Signatures of the public key.
|
||||
* userId -> (deviceSignKeyId -> signature)
|
||||
*/
|
||||
@Json(name = "signatures")
|
||||
val signatures: Map<String, Map<String, String>>? = null
|
||||
) {
|
||||
|
||||
internal fun toJsonDict(): JsonDict {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val adapter = moshi.adapter(Map::class.java)
|
||||
|
||||
return moshi
|
||||
.adapter(MegolmBackupAuthData::class.java)
|
||||
.toJson(this)
|
||||
.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
adapter.fromJson(it) as JsonDict
|
||||
}
|
||||
}
|
||||
|
||||
internal fun signalableJSONDictionary(): JsonDict {
|
||||
return SignalableMegolmBackupAuthData(
|
||||
publicKey = publicKey,
|
||||
privateKeySalt = privateKeySalt,
|
||||
privateKeyIterations = privateKeyIterations
|
||||
)
|
||||
.signalableJSONDictionary()
|
||||
}
|
||||
fun toJsonDict(): JsonDict
|
||||
fun copy(newSignatures: Map<String, Map<String, String>>?): MegolmBackupAuthData
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.keysbackup
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Data model for [org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData.authData] in case
|
||||
* of [org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MegolmBackupCurve25519AuthData(
|
||||
/**
|
||||
* The curve25519 public key used to encrypt the backups.
|
||||
*/
|
||||
@Json(name = "public_key")
|
||||
val publicKey: String? = null,
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the salt associated with the backup
|
||||
* private key.
|
||||
*/
|
||||
@Json(name = "private_key_salt")
|
||||
override val privateKeySalt: String? = null,
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the number of key derivations.
|
||||
*/
|
||||
@Json(name = "private_key_iterations")
|
||||
override val privateKeyIterations: Int? = null,
|
||||
|
||||
/**
|
||||
* Signatures of the public key.
|
||||
* userId -> (deviceSignKeyId -> signature)
|
||||
*/
|
||||
@Json(name = "signatures")
|
||||
override val signatures: Map<String, Map<String, String>>? = null
|
||||
) : MegolmBackupAuthData {
|
||||
|
||||
override fun isValid(): Boolean = !publicKey.isNullOrEmpty()
|
||||
|
||||
override fun copy(newSignatures: Map<String, Map<String, String>>?): MegolmBackupAuthData {
|
||||
return copy(signatures = newSignatures)
|
||||
}
|
||||
|
||||
override fun toJsonDict(): JsonDict {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val adapter = moshi.adapter(Map::class.java)
|
||||
|
||||
return moshi
|
||||
.adapter(MegolmBackupCurve25519AuthData::class.java)
|
||||
.toJson(this)
|
||||
.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
adapter.fromJson(it) as JsonDict
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ internal class DefaultSessionParamsCreator @Inject constructor(
|
||||
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
|
||||
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
||||
?.let { Uri.parse(it) }
|
||||
?.takeIf { validateUri(it, homeServerConnectionConfig) }
|
||||
// ?.takeIf { validateUri(it, homeServerConnectionConfig) }
|
||||
|
||||
private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
|
||||
// Validate the URL, if the configuration is wrong server side, do not override
|
||||
|
@ -78,4 +78,10 @@ internal data class MegolmSessionData(
|
||||
// When this feature lands in spec name = shared_history should be used
|
||||
@Json(name = "org.matrix.msc3061.shared_history")
|
||||
val sharedHistory: Boolean = false,
|
||||
|
||||
/**
|
||||
* Flag indicating that this session data is untrusted.
|
||||
*/
|
||||
@Json(name = "untrusted")
|
||||
val untrusted: Boolean = false,
|
||||
)
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
@ -27,7 +26,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
@ -47,18 +45,18 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.algorithm.KeysBackupAlgorithm
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.algorithm.KeysBackupAlgorithmFactory
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
||||
@ -77,7 +75,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupV
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
@ -87,11 +84,7 @@ import org.matrix.android.sdk.internal.task.TaskThread
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.olm.OlmException
|
||||
import org.matrix.olm.OlmPkDecryption
|
||||
import org.matrix.olm.OlmPkEncryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidParameterException
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -99,6 +92,9 @@ import kotlin.random.Random
|
||||
* A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys)
|
||||
* to the user's homeserver.
|
||||
*/
|
||||
|
||||
private const val DEFAULT_ALGORITHM = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultKeysBackupService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@ -121,22 +117,20 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||
// Task executor
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val algorithmFactory: KeysBackupAlgorithmFactory,
|
||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val prepareKeysBackup: PrepareKeysBackupUseCase,
|
||||
private val keysBackupStateManager: KeysBackupStateManager,
|
||||
private val uiHandler: Handler,
|
||||
) : KeysBackupService {
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val keysBackupStateManager = KeysBackupStateManager(uiHandler)
|
||||
|
||||
// The backup version
|
||||
override var keysBackupVersion: KeysVersionResult? = null
|
||||
private set
|
||||
|
||||
// The backup key being used.
|
||||
private var backupOlmPkEncryption: OlmPkEncryption? = null
|
||||
private var algorithm: KeysBackupAlgorithm? = null
|
||||
|
||||
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
|
||||
|
||||
@ -158,79 +152,17 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
override fun prepareKeysBackupVersion(
|
||||
password: String?,
|
||||
algorithm: String?,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||
) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
try {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
val signalableMegolmBackupAuthData = if (password != null) {
|
||||
// Generate a private key from the password
|
||||
val backgroundProgressListener = if (progressListener == null) {
|
||||
null
|
||||
} else {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
uiHandler.post {
|
||||
try {
|
||||
progressListener.onProgress(progress, total)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||
privateKeySalt = generatePrivateKeyResult.salt,
|
||||
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||
)
|
||||
} else {
|
||||
val publicKey = olmPkDecryption.generateKey()
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||
|
||||
val signatures = mutableMapOf<String, MutableMap<String, String>>()
|
||||
|
||||
val deviceSignature = objectSigner.signObject(canonicalJson)
|
||||
deviceSignature.forEach { (userID, content) ->
|
||||
signatures[userID] = content.toMutableMap()
|
||||
}
|
||||
|
||||
// If we have cross signing add signature, will throw if cross signing not properly configured
|
||||
try {
|
||||
val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson)
|
||||
signatures[credentials.userId]?.putAll(crossSign)
|
||||
} catch (failure: Throwable) {
|
||||
// ignore and log
|
||||
Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys")
|
||||
}
|
||||
|
||||
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
||||
signatures = signatures
|
||||
)
|
||||
val creationInfo = MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP,
|
||||
authData = signedMegolmBackupAuthData,
|
||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||
)
|
||||
uiHandler.post {
|
||||
callback.onSuccess(creationInfo)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
uiHandler.post {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
cryptoCoroutineScope.launch {
|
||||
prepareKeysBackup(
|
||||
algorithm = algorithm ?: DEFAULT_ALGORITHM,
|
||||
password = password,
|
||||
progressListener = progressListener,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,7 +284,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<Unit>?
|
||||
) {
|
||||
if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) {
|
||||
if (!isEnabled() || algorithm == null || keysBackupVersion == null) {
|
||||
callback?.onFailure(Throwable("Backup not enabled"))
|
||||
return
|
||||
}
|
||||
@ -430,12 +362,12 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
||||
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
||||
|
||||
if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
|
||||
if (authData == null || authData.signatures.isNullOrEmpty()) {
|
||||
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
||||
return KeysBackupVersionTrust(usable = false)
|
||||
}
|
||||
|
||||
val mySigs = authData.signatures[userId]
|
||||
val signatures = authData.signatures!!
|
||||
val mySigs = authData.signatures?.get(userId)
|
||||
if (mySigs.isNullOrEmpty()) {
|
||||
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
||||
return KeysBackupVersionTrust(usable = false)
|
||||
@ -459,7 +391,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
var isSignatureValid = false
|
||||
try {
|
||||
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures)
|
||||
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.toSignalableJsonDict(), signatures)
|
||||
isSignatureValid = true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK")
|
||||
@ -485,7 +417,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
val fingerprint = device.fingerprint()
|
||||
if (fingerprint != null) {
|
||||
try {
|
||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
||||
olmDevice.verifySignature(fingerprint, authData.toSignalableJsonDict(), mySignature)
|
||||
isSignatureValid = true
|
||||
} catch (e: OlmException) {
|
||||
Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
||||
@ -520,7 +452,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
|
||||
|
||||
// Get auth data to update it
|
||||
val authData = getMegolmBackupAuthData(keysBackupVersion)
|
||||
val authData = keysBackupVersion.getValidAuthData()
|
||||
|
||||
if (authData == null) {
|
||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||
@ -535,7 +467,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
if (trust) {
|
||||
// Add current device signature
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.toSignalableJsonDict())
|
||||
|
||||
val deviceSignatures = objectSigner.signObject(canonicalJson)
|
||||
|
||||
@ -548,14 +480,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
}
|
||||
|
||||
// Create an updated version of KeysVersionResult
|
||||
val newMegolmBackupAuthData = authData.copy()
|
||||
|
||||
val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap()
|
||||
val newSignatures = authData.signatures.orEmpty().toMutableMap()
|
||||
newSignatures[userId] = myUserSignatures
|
||||
|
||||
val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy(
|
||||
signatures = newSignatures
|
||||
)
|
||||
val newMegolmBackupAuthDataWithNewSignature = authData.copy(newSignatures)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
UpdateKeysBackupVersionBody(
|
||||
@ -666,36 +594,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key from a Recovery key.
|
||||
*
|
||||
* @param recoveryKey the recovery key
|
||||
* @return the corresponding public key, from Olm
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? {
|
||||
// Extract the primary key
|
||||
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
||||
|
||||
if (privateKey == null) {
|
||||
Timber.w("pkPublicKeyFromRecoveryKey: private key is null")
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Built the PK decryption with it
|
||||
val pkPublicKey: String
|
||||
|
||||
try {
|
||||
val decryption = OlmPkDecryption()
|
||||
pkPublicKey = decryption.setPrivateKey(privateKey)
|
||||
} catch (e: OlmException) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pkPublicKey
|
||||
}
|
||||
|
||||
private fun resetBackupAllGroupSessionsListeners() {
|
||||
backupAllGroupSessionsCallback = null
|
||||
|
||||
@ -725,79 +623,44 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val decryption = withContext(coroutineDispatchers.computation) {
|
||||
// Check if the recovery is valid before going any further
|
||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
// Get a PK decryption instance
|
||||
pkDecryptionFromRecoveryKey(recoveryKey)
|
||||
}
|
||||
if (decryption == null) {
|
||||
// This should not happen anymore
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
|
||||
// Save for next time and for gossiping
|
||||
// Save now as it's valid, don't wait for the import as it could take long.
|
||||
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
||||
|
||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||
|
||||
// Get backed up keys from the homeserver
|
||||
val data = getKeys(sessionId, roomId, keysVersionResult.version)
|
||||
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
val sessionsData = ArrayList<MegolmSessionData>()
|
||||
// Restore that data
|
||||
var sessionsFromHsCount = 0
|
||||
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
|
||||
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
|
||||
sessionsFromHsCount++
|
||||
|
||||
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
||||
|
||||
sessionData?.let {
|
||||
sessionsData.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
val sessionsData = withContext(coroutineDispatchers.computation) {
|
||||
algorithm?.decryptSessions(recoveryKey, data)
|
||||
}.orEmpty()
|
||||
// Do not trigger a backup for them if they come from the backup version we are using
|
||||
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
||||
if (backUp) {
|
||||
Timber.v(
|
||||
"restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
|
||||
" of $sessionsFromHsCount from the backup store on the homeserver"
|
||||
"restoreKeysWithRecoveryKey: Those keys will be backed up" +
|
||||
" to backup version: ${keysBackupVersion?.version}"
|
||||
)
|
||||
|
||||
// Do not trigger a backup for them if they come from the backup version we are using
|
||||
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
||||
if (backUp) {
|
||||
Timber.v(
|
||||
"restoreKeysWithRecoveryKey: Those keys will be backed up" +
|
||||
" to backup version: ${keysBackupVersion?.version}"
|
||||
)
|
||||
}
|
||||
|
||||
// Import them into the crypto store
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
|
||||
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener)
|
||||
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (backUp) {
|
||||
maybeBackupKeys()
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Import them into the crypto store
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
|
||||
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener)
|
||||
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (backUp) {
|
||||
maybeBackupKeys()
|
||||
}
|
||||
result
|
||||
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||
uiHandler.post {
|
||||
@ -902,26 +765,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? {
|
||||
// Extract the primary key
|
||||
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
||||
|
||||
// Built the PK decryption with it
|
||||
var decryption: OlmPkDecryption? = null
|
||||
if (privateKey != null) {
|
||||
try {
|
||||
decryption = OlmPkDecryption()
|
||||
decryption.setPrivateKey(privateKey)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
}
|
||||
|
||||
return decryption
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a backup if there are new keys, with a delay.
|
||||
*/
|
||||
@ -1103,15 +946,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
/**
|
||||
* Extract MegolmBackupAuthData data from a backup version.
|
||||
*
|
||||
* @param keysBackupData the key backup data
|
||||
*
|
||||
* @return the authentication if found and valid, null in other case
|
||||
*/
|
||||
private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? {
|
||||
return keysBackupData
|
||||
.takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP }
|
||||
?.getAuthDataAsMegolmBackupAuthData()
|
||||
?.takeIf { it.publicKey.isNotEmpty() }
|
||||
private fun KeysVersionResult.getValidAuthData(): MegolmBackupAuthData? {
|
||||
return getAuthDataAsMegolmBackupAuthData()
|
||||
?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1125,7 +964,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? {
|
||||
val authData = getMegolmBackupAuthData(keysBackupData)
|
||||
val authData = keysBackupData.getValidAuthData()
|
||||
|
||||
if (authData == null) {
|
||||
Timber.w("recoveryKeyFromPassword: invalid parameter")
|
||||
@ -1139,8 +978,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
val salt = authData.privateKeySalt!!
|
||||
val iterations = authData.privateKeyIterations!!
|
||||
// Extract the recovery key from the passphrase
|
||||
val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt, authData.privateKeyIterations, progressListener)
|
||||
val data = retrievePrivateKeyWithPassword(password, salt, iterations, progressListener)
|
||||
|
||||
return computeRecoveryKey(data)
|
||||
}
|
||||
@ -1155,32 +996,15 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean {
|
||||
// Build PK decryption instance with the recovery key
|
||||
val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey)
|
||||
|
||||
if (publicKey == null) {
|
||||
Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null")
|
||||
|
||||
return false
|
||||
return try {
|
||||
val algorithm = algorithmFactory.create(keysBackupData)
|
||||
val isValid = algorithm.keyMatches(recoveryKey)
|
||||
algorithm.release()
|
||||
isValid
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Can't check validity of recoveryKey")
|
||||
false
|
||||
}
|
||||
|
||||
val authData = getMegolmBackupAuthData(keysBackupData)
|
||||
|
||||
if (authData == null) {
|
||||
Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare both
|
||||
if (publicKey != authData.publicKey) {
|
||||
Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Public keys match!
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
|
||||
@ -1220,11 +1044,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
||||
|
||||
try {
|
||||
backupOlmPkEncryption = OlmPkEncryption().apply {
|
||||
setRecipientKey(retrievedMegolmBackupAuthData.publicKey)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
algorithm = algorithmFactory.create(keysVersionResult)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while creating algorithm")
|
||||
keysBackupStateManager.state = KeysBackupState.Disabled
|
||||
return
|
||||
}
|
||||
@ -1260,8 +1082,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
cryptoStore.setKeyBackupVersion(null)
|
||||
cryptoStore.setKeysBackupData(null)
|
||||
backupOlmPkEncryption?.releaseEncryption()
|
||||
backupOlmPkEncryption = null
|
||||
algorithm?.release()
|
||||
algorithm = null
|
||||
|
||||
// Reset backup markers
|
||||
cryptoStore.resetBackupMarkers()
|
||||
@ -1275,7 +1097,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
Timber.v("backupKeys")
|
||||
|
||||
// Sanity check, as this method can be called after a delay, the state may have change during the delay
|
||||
if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) {
|
||||
if (!isEnabled() || algorithm == null || keysBackupVersion == null) {
|
||||
Timber.v("backupKeys: Invalid configuration")
|
||||
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
|
||||
resetBackupAllGroupSessionsListeners()
|
||||
@ -1411,8 +1233,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
// Gather information for each key
|
||||
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey)
|
||||
|
||||
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
|
||||
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
|
||||
val sessionData = inboundGroupSessionStore
|
||||
.getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
|
||||
?.let {
|
||||
@ -1421,30 +1241,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
}
|
||||
}
|
||||
?: return null
|
||||
val sessionBackupData = mapOf(
|
||||
"algorithm" to sessionData.algorithm,
|
||||
"sender_key" to sessionData.senderKey,
|
||||
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
||||
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
|
||||
"session_key" to sessionData.sessionKey,
|
||||
"org.matrix.msc3061.shared_history" to sessionData.sharedHistory
|
||||
)
|
||||
|
||||
val json = MoshiProvider.providesMoshi()
|
||||
.adapter(Map::class.java)
|
||||
.toJson(sessionBackupData)
|
||||
|
||||
val encryptedSessionBackupData = try {
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
backupOlmPkEncryption?.encrypt(json)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
null
|
||||
}
|
||||
?: return null
|
||||
|
||||
// Build backup data for that key
|
||||
val sessionBackupData = algorithm?.encryptSession(sessionData) ?: return null
|
||||
return KeyBackupData(
|
||||
firstMessageIndex = try {
|
||||
olmInboundGroupSessionWrapper.session.firstKnownIndex
|
||||
@ -1455,11 +1253,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
|
||||
isVerified = device?.isVerified == true,
|
||||
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
|
||||
sessionData = mapOf(
|
||||
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
||||
"mac" to encryptedSessionBackupData.mMac,
|
||||
"ephemeral" to encryptedSessionBackupData.mEphemeralKey
|
||||
)
|
||||
sessionData = sessionBackupData
|
||||
)
|
||||
}
|
||||
|
||||
@ -1471,45 +1265,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
return sessionData.sharedHistory
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
||||
var sessionBackupData: MegolmSessionData? = null
|
||||
|
||||
val jsonObject = keyBackupData.sessionData
|
||||
|
||||
val ciphertext = jsonObject["ciphertext"]?.toString()
|
||||
val mac = jsonObject["mac"]?.toString()
|
||||
val ephemeralKey = jsonObject["ephemeral"]?.toString()
|
||||
|
||||
if (ciphertext != null && mac != null && ephemeralKey != null) {
|
||||
val encrypted = OlmPkMessage()
|
||||
encrypted.mCipherText = ciphertext
|
||||
encrypted.mMac = mac
|
||||
encrypted.mEphemeralKey = ephemeralKey
|
||||
|
||||
try {
|
||||
val decrypted = decryption.decrypt(encrypted)
|
||||
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val adapter = moshi.adapter(MegolmSessionData::class.java)
|
||||
|
||||
sessionBackupData = adapter.fromJson(decrypted)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
|
||||
if (sessionBackupData != null) {
|
||||
sessionBackupData = sessionBackupData.copy(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return sessionBackupData
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
* ========================================================================================== */
|
||||
@ -1559,3 +1314,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||
|
||||
override fun toString() = "KeysBackup for $userId"
|
||||
}
|
||||
|
||||
internal fun MegolmBackupAuthData.toSignalableJsonDict(): JsonDict {
|
||||
return HashMap(toJsonDict()).apply {
|
||||
remove("signatures")
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,9 @@ import android.os.Handler
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class KeysBackupStateManager(private val uiHandler: Handler) {
|
||||
internal class KeysBackupStateManager @Inject constructor(private val uiHandler: Handler) {
|
||||
|
||||
private val listeners = ArrayList<KeysBackupStateListener>()
|
||||
|
||||
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup
|
||||
|
||||
import android.os.Handler
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
import org.matrix.olm.OlmPkDecryption
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PrepareKeysBackupUseCase @Inject constructor(
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val objectSigner: ObjectSigner,
|
||||
private val credentials: Credentials,
|
||||
private val crossSigningOlm: CrossSigningOlm,
|
||||
private val uiHandler: Handler,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
algorithm: String,
|
||||
password: String?,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||
) = withContext(coroutineDispatchers.io) {
|
||||
when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP -> prepareCurve(password, progressListener, callback)
|
||||
/*
|
||||
MXCRYPTO_ALGORITHM_AES_256_BACKUP -> prepareAES(password, progressListener, callback)
|
||||
*/
|
||||
else -> {
|
||||
callback.onFailure(IllegalStateException("Unknown algorithm"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareCurve(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
try {
|
||||
val signalableMegolmBackupAuthData = if (password != null) {
|
||||
// Generate a private key from the password
|
||||
val backgroundProgressListener = if (progressListener == null) {
|
||||
null
|
||||
} else {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
uiHandler.post {
|
||||
try {
|
||||
progressListener.onProgress(progress, total)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||
privateKeySalt = generatePrivateKeyResult.salt,
|
||||
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||
)
|
||||
} else {
|
||||
val publicKey = olmPkDecryption.generateKey()
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||
|
||||
val signatures = mutableMapOf<String, MutableMap<String, String>>()
|
||||
|
||||
val deviceSignature = objectSigner.signObject(canonicalJson)
|
||||
deviceSignature.forEach { (userID, content) ->
|
||||
signatures[userID] = content.toMutableMap()
|
||||
}
|
||||
|
||||
// If we have cross signing add signature, will throw if cross signing not properly configured
|
||||
try {
|
||||
val crossSign = crossSigningOlm.signObject(CrossSigningOlm.KeyType.MASTER, canonicalJson)
|
||||
signatures[credentials.userId]?.putAll(crossSign)
|
||||
} catch (failure: Throwable) {
|
||||
// ignore and log
|
||||
Timber.w(failure, "prepareKeysBackupVersion: failed to sign with cross signing keys")
|
||||
}
|
||||
|
||||
val signedMegolmBackupCurve25519AuthData = MegolmBackupCurve25519AuthData(
|
||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
||||
signatures = signatures
|
||||
)
|
||||
val creationInfo = MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP,
|
||||
authData = signedMegolmBackupCurve25519AuthData,
|
||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||
)
|
||||
uiHandler.post {
|
||||
callback.onSuccess(creationInfo)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
uiHandler.post {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
} finally {
|
||||
olmPkDecryption.releaseDecryption()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private fun prepareAES(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup.algorithm
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||
|
||||
internal interface KeysBackupAlgorithm {
|
||||
val authData: MegolmBackupAuthData
|
||||
val untrusted: Boolean
|
||||
|
||||
fun encryptSession(sessionData: MegolmSessionData): JsonDict?
|
||||
fun decryptSessions(recoveryKey: String, data: KeysBackupData): List<MegolmSessionData>
|
||||
fun keyMatches(key: String): Boolean
|
||||
fun release()
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup.algorithm
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class KeysBackupAlgorithmFactory @Inject constructor() {
|
||||
|
||||
fun create(keysVersion: KeysVersionResult): KeysBackupAlgorithm {
|
||||
return when (keysVersion.algorithm) {
|
||||
MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP -> {
|
||||
KeysBackupCurve25519Algorithm(keysVersion)
|
||||
}
|
||||
MXCRYPTO_ALGORITHM_AES_256_BACKUP -> {
|
||||
throw IllegalStateException("AES_256 is not yet handled")
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Unknown algorithm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup.algorithm
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.olm.OlmException
|
||||
import org.matrix.olm.OlmPkDecryption
|
||||
import org.matrix.olm.OlmPkEncryption
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidParameterException
|
||||
|
||||
internal class KeysBackupCurve25519Algorithm(keysVersions: KeysVersionResult) : KeysBackupAlgorithm {
|
||||
|
||||
override val untrusted: Boolean = true
|
||||
|
||||
private val curveAuthData: MegolmBackupCurve25519AuthData
|
||||
private val publicKey: String
|
||||
|
||||
private val encryptionKey: OlmPkEncryption
|
||||
|
||||
init {
|
||||
if (keysVersions.algorithm != MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP) {
|
||||
throw IllegalStateException("Algorithm doesn't match")
|
||||
}
|
||||
curveAuthData = keysVersions.getAuthDataAsMegolmBackupAuthData() as MegolmBackupCurve25519AuthData
|
||||
publicKey = curveAuthData.publicKey ?: throw IllegalStateException("No public key")
|
||||
encryptionKey = OlmPkEncryption().apply {
|
||||
setRecipientKey(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
|
||||
val sessionBackupData = mapOf(
|
||||
"algorithm" to sessionData.algorithm,
|
||||
"sender_key" to sessionData.senderKey,
|
||||
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
||||
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
|
||||
"session_key" to sessionData.sessionKey,
|
||||
"org.matrix.msc3061.shared_history" to sessionData.sharedHistory,
|
||||
"untrusted" to sessionData.untrusted
|
||||
)
|
||||
|
||||
val json = MoshiProvider.providesMoshi()
|
||||
.adapter(Map::class.java)
|
||||
.toJson(sessionBackupData)
|
||||
|
||||
val encryptedSessionBackupData = try {
|
||||
encryptionKey.encrypt(json)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "Error while encrypting backup data.")
|
||||
null
|
||||
} ?: return null
|
||||
|
||||
return mapOf(
|
||||
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
||||
"mac" to encryptedSessionBackupData.mMac,
|
||||
"ephemeral" to encryptedSessionBackupData.mEphemeralKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun decryptSessions(recoveryKey: String, data: KeysBackupData): List<MegolmSessionData> {
|
||||
fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? {
|
||||
// Extract the primary key
|
||||
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
||||
// Built the PK decryption with it
|
||||
var decryption: OlmPkDecryption? = null
|
||||
if (privateKey != null) {
|
||||
try {
|
||||
decryption = OlmPkDecryption()
|
||||
decryption.setPrivateKey(privateKey)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
}
|
||||
return decryption
|
||||
}
|
||||
|
||||
if (!keyMatches(recoveryKey)) {
|
||||
Timber.e("Invalid recovery key for this keys version")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
// Get a PK decryption instance
|
||||
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
|
||||
if (decryption == null) {
|
||||
// This should not happen anymore
|
||||
Timber.e("Invalid recovery key. Error")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
val sessionsData = ArrayList<MegolmSessionData>()
|
||||
// Restore that data
|
||||
var sessionsFromHsCount = 0
|
||||
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
|
||||
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
|
||||
sessionsFromHsCount++
|
||||
val sessionData = decryptSession(keyBackupData.sessionData, sessionIdLoop, roomIdLoop, decryption)
|
||||
sessionData?.let {
|
||||
sessionsData.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v(
|
||||
"Decrypted ${sessionsData.size} keys out of $sessionsFromHsCount from the backup store on the homeserver"
|
||||
)
|
||||
return sessionsData
|
||||
}
|
||||
|
||||
private fun decryptSession(sessionData: JsonDict, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
||||
var sessionBackupData: MegolmSessionData? = null
|
||||
|
||||
val ciphertext = sessionData["ciphertext"]?.toString()
|
||||
val mac = sessionData["mac"]?.toString()
|
||||
val ephemeralKey = sessionData["ephemeral"]?.toString()
|
||||
|
||||
if (ciphertext != null && mac != null && ephemeralKey != null) {
|
||||
val encrypted = OlmPkMessage()
|
||||
encrypted.mCipherText = ciphertext
|
||||
encrypted.mMac = mac
|
||||
encrypted.mEphemeralKey = ephemeralKey
|
||||
|
||||
try {
|
||||
val decrypted = decryption.decrypt(encrypted)
|
||||
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val adapter = moshi.adapter(MegolmSessionData::class.java)
|
||||
|
||||
sessionBackupData = adapter.fromJson(decrypted)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
|
||||
if (sessionBackupData != null) {
|
||||
sessionBackupData = sessionBackupData.copy(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
untrusted = untrusted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return sessionBackupData
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
encryptionKey.releaseEncryption()
|
||||
}
|
||||
|
||||
override val authData: MegolmBackupAuthData = curveAuthData
|
||||
|
||||
override fun keyMatches(key: String): Boolean {
|
||||
fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? {
|
||||
// Extract the primary key
|
||||
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
||||
if (privateKey == null) {
|
||||
Timber.w("pkPublicKeyFromRecoveryKey: private key is null")
|
||||
return null
|
||||
}
|
||||
// Built the PK decryption with it
|
||||
val decryption = OlmPkDecryption()
|
||||
val pkPublicKey = try {
|
||||
decryption.setPrivateKey(privateKey)
|
||||
} catch (e: OlmException) {
|
||||
null
|
||||
} finally {
|
||||
decryption.releaseDecryption()
|
||||
}
|
||||
return pkPublicKey
|
||||
}
|
||||
|
||||
val publicKey = pkPublicKeyFromRecoveryKey(key)
|
||||
if (publicKey == null) {
|
||||
Timber.w("Public key is null")
|
||||
return false
|
||||
}
|
||||
// Compare both
|
||||
if (publicKey != this.publicKey) {
|
||||
Timber.w("Public keys mismatch")
|
||||
return false
|
||||
}
|
||||
// Public keys match!
|
||||
return true
|
||||
}
|
||||
}
|
@ -23,7 +23,9 @@ import org.matrix.android.sdk.api.util.JsonDict
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class CreateKeysBackupVersionBody(
|
||||
/**
|
||||
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined.
|
||||
* The algorithm used for storing backups.
|
||||
* Currently, "m.megolm_backup.v1.curve25519-aes-sha2" and
|
||||
* org.matrix.msc3270.v1.aes-hmac-sha2 are defined.
|
||||
*/
|
||||
@Json(name = "algorithm")
|
||||
override val algorithm: String,
|
||||
|
@ -16,8 +16,11 @@
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAes256AuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
@ -41,12 +44,14 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
internal interface KeysAlgorithmAndData {
|
||||
|
||||
/**
|
||||
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined.
|
||||
* The algorithm used for storing backups.
|
||||
* Currently, "m.megolm_backup.v1.curve25519-aes-sha2" and
|
||||
* org.matrix.msc3270.v1.aes-hmac-sha2 are defined.
|
||||
*/
|
||||
val algorithm: String
|
||||
|
||||
/**
|
||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData].
|
||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" * see [org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData].
|
||||
*/
|
||||
val authData: JsonDict
|
||||
|
||||
@ -54,9 +59,12 @@ internal interface KeysAlgorithmAndData {
|
||||
* Facility method to convert authData to a MegolmBackupAuthData object.
|
||||
*/
|
||||
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.takeIf { algorithm == MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP }
|
||||
?.adapter(MegolmBackupAuthData::class.java)
|
||||
?.fromJsonValue(authData)
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP -> moshi.adapter(MegolmBackupCurve25519AuthData::class.java)
|
||||
MXCRYPTO_ALGORITHM_AES_256_BACKUP -> moshi.adapter(MegolmBackupAes256AuthData::class.java)
|
||||
else -> null
|
||||
}
|
||||
return moshiAdapter?.fromJsonValue(authData)
|
||||
}
|
||||
}
|
||||
|
@ -23,14 +23,16 @@ import org.matrix.android.sdk.api.util.JsonDict
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UpdateKeysBackupVersionBody(
|
||||
/**
|
||||
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
|
||||
* The algorithm used for storing backups.
|
||||
* Currently, "m.megolm_backup.v1.curve25519-aes-sha2" and
|
||||
* org.matrix.msc3270.v1.aes-hmac-sha2 are defined.
|
||||
*/
|
||||
@Json(name = "algorithm")
|
||||
override val algorithm: String,
|
||||
|
||||
/**
|
||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2".
|
||||
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||
* see [org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData]
|
||||
*/
|
||||
@Json(name = "auth_data")
|
||||
override val authData: JsonDict,
|
||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Handler
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
@ -66,6 +67,8 @@ internal interface MatrixComponent {
|
||||
|
||||
fun moshi(): Moshi
|
||||
|
||||
fun uiHandler(): Handler
|
||||
|
||||
@Unauthenticated
|
||||
fun okHttpClient(): OkHttpClient
|
||||
|
||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Handler
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -25,6 +26,7 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.createBackgroundHandler
|
||||
import org.matrix.android.sdk.internal.util.createUIHandler
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.File
|
||||
import java.util.concurrent.Executors
|
||||
@ -51,6 +53,12 @@ internal object MatrixModule {
|
||||
return context.resources
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
fun providesUIHandler(): Handler {
|
||||
return createUIHandler()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CacheDirectory
|
||||
|
@ -144,7 +144,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(
|
||||
// for the backup and store it in the 4S
|
||||
if (session.sharedSecretStorageService().isRecoverySetup()) {
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||
}
|
||||
pendingBackupCreationInfo = creationInfo
|
||||
val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()
|
||||
|
@ -97,7 +97,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor(
|
||||
val requestedId = currentRequestId.value!!
|
||||
|
||||
mxSession.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase,
|
||||
object : ProgressListener {
|
||||
progressListener = object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
if (requestedId != currentRequestId.value) {
|
||||
// this is an old request, we can't cancel but we can ignore
|
||||
@ -111,7 +111,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
},
|
||||
object : MatrixCallback<MegolmBackupCreationInfo> {
|
||||
callback = object : MatrixCallback<MegolmBackupCreationInfo> {
|
||||
override fun onSuccess(data: MegolmBackupCreationInfo) {
|
||||
if (requestedId != currentRequestId.value) {
|
||||
// this is an old request, we can't cancel but we can ignore
|
||||
|
@ -247,7 +247,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||
|
||||
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||
}
|
||||
val version = awaitCallback<KeysVersion> {
|
||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||
|
Loading…
x
Reference in New Issue
Block a user