Compare commits
10 Commits
develop
...
feature/fg
Author | SHA1 | Date | |
---|---|---|---|
|
07ec899b49 | ||
|
77bc5e5b7a | ||
|
e5f19945bd | ||
|
b63e3d69ed | ||
|
1db5c662d2 | ||
|
03272a9c8f | ||
|
1d26207df7 | ||
|
92faf3bca3 | ||
|
c055f40607 | ||
|
f855664504 |
@ -20,12 +20,14 @@ import android.util.Log
|
|||||||
import org.amshove.kluent.fail
|
import org.amshove.kluent.fail
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
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.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
@ -34,15 +36,17 @@ 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.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.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.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.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.keysbackup.extractCurveKeyFromRecoveryKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
@ -52,6 +56,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
||||||
import org.matrix.android.sdk.api.session.securestorage.KeyRef
|
import org.matrix.android.sdk.api.session.securestorage.KeyRef
|
||||||
|
import org.matrix.android.sdk.api.util.awaitCallback
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
@ -178,8 +183,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
|
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
|
||||||
return MegolmBackupAuthData(
|
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",
|
publicKey = "abcdefg",
|
||||||
signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop"))
|
signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop"))
|
||||||
)
|
)
|
||||||
@ -187,7 +219,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||||||
|
|
||||||
fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo {
|
fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo {
|
||||||
return MegolmBackupCreationInfo(
|
return MegolmBackupCreationInfo(
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
algorithm = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP,
|
||||||
authData = createFakeMegolmBackupAuthData(),
|
authData = createFakeMegolmBackupAuthData(),
|
||||||
recoveryKey = "fake"
|
recoveryKey = "fake"
|
||||||
)
|
)
|
||||||
@ -272,10 +304,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// set up megolm backup
|
// set up megolm backup
|
||||||
val creationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||||
}
|
}
|
||||||
val version = testHelper.waitForCallback<KeysVersion> {
|
val version = awaitCallback<KeysVersion> {
|
||||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||||
}
|
}
|
||||||
// Save it for gossiping
|
// Save it for gossiping
|
||||||
|
@ -218,7 +218,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
|
|||||||
val keysBackupService = aliceSession.cryptoService().keysBackupService()
|
val keysBackupService = aliceSession.cryptoService().keysBackupService()
|
||||||
val keyBackupPassword = "FooBarBaz"
|
val keyBackupPassword = "FooBarBaz"
|
||||||
val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||||
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, null, it)
|
||||||
}
|
}
|
||||||
val version = commonTestHelper.waitForCallback<KeysVersion> {
|
val version = commonTestHelper.waitForCallback<KeysVersion> {
|
||||||
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
|
@ -224,7 +224,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||||||
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
|
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
|
||||||
val keyBackupPassword = "FooBarBaz"
|
val keyBackupPassword = "FooBarBaz"
|
||||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||||
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, null, it)
|
||||||
}
|
}
|
||||||
val version = testHelper.waitForCallback<KeysVersion> {
|
val version = testHelper.waitForCallback<KeysVersion> {
|
||||||
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
|
@ -29,10 +29,11 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
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.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeyBackupConfig
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||||
@ -41,14 +42,23 @@ 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.KeysVersion
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
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.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.keysbackup.toKeysVersionResult
|
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.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
import org.matrix.android.sdk.common.RetryTestRule
|
import org.matrix.android.sdk.common.RetryTestRule
|
||||||
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import org.matrix.android.sdk.common.waitFor
|
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.security.InvalidParameterException
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
@ -57,10 +67,21 @@ import kotlin.coroutines.resume
|
|||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class KeysBackupTest : InstrumentedTest {
|
open class KeysBackupTest : InstrumentedTest {
|
||||||
|
|
||||||
@get:Rule val rule = RetryTestRule(3)
|
@get:Rule val rule = RetryTestRule(3)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun default_config_should_be_assymetric_only() = runSessionTest(context()) { testHelper ->
|
||||||
|
val session = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
|
||||||
|
val defaultConfig = session.cryptoService().keysBackupService().keyBackupConfig
|
||||||
|
|
||||||
|
assertEquals(MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP, defaultConfig.defaultAlgorithm)
|
||||||
|
assertEquals(1, defaultConfig.supportedAlgorithms.size)
|
||||||
|
assertTrue(defaultConfig.supportedAlgorithms.contains(MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
|
||||||
* - Check backup keys after having marked one as backed up
|
* - Check backup keys after having marked one as backed up
|
||||||
@ -119,11 +140,12 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertFalse(keysBackup.isEnabled())
|
assertFalse(keysBackup.isEnabled())
|
||||||
|
|
||||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||||
keysBackup.prepareKeysBackupVersion(null, null, it)
|
keysBackup.prepareKeysBackupVersion(null, null, null, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm)
|
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.authData.signatures)
|
||||||
assertNotNull(megolmBackupCreationInfo.recoveryKey)
|
assertNotNull(megolmBackupCreationInfo.recoveryKey)
|
||||||
|
|
||||||
@ -145,7 +167,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
assertFalse(keysBackup.isEnabled())
|
assertFalse(keysBackup.isEnabled())
|
||||||
|
|
||||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||||
keysBackup.prepareKeysBackupVersion(null, null, it)
|
keysBackup.prepareKeysBackupVersion(null, null, null, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertFalse(keysBackup.isEnabled())
|
assertFalse(keysBackup.isEnabled())
|
||||||
@ -200,7 +222,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -241,7 +263,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -286,7 +308,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
|
|
||||||
@ -298,23 +320,28 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
|
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
|
||||||
|
|
||||||
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
||||||
|
val keysBackupVersion = keysBackup.keysBackupVersion
|
||||||
|
assertNotNull(keysBackupVersion)
|
||||||
|
val algorithm = KeysBackupAlgorithmFactory().create(keysBackupVersion!!)
|
||||||
// - Check encryptGroupSession() returns stg
|
// - Check encryptGroupSession() returns stg
|
||||||
val keyBackupData = keysBackup.encryptGroupSession(session)
|
val keyBackupData = keysBackup.encryptGroupSession(session)
|
||||||
assertNotNull(keyBackupData)
|
assertNotNull(keyBackupData)
|
||||||
assertNotNull(keyBackupData!!.sessionData)
|
assertNotNull(keyBackupData!!.sessionData)
|
||||||
|
val roomKeysBackupData = RoomKeysBackupData(
|
||||||
// - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
|
HashMap<String, KeyBackupData>().apply {
|
||||||
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
|
put(session.safeSessionId!!, keyBackupData)
|
||||||
assertNotNull(decryption)
|
}
|
||||||
// - Check decryptKeyBackupData() returns stg
|
|
||||||
val sessionData = keysBackup
|
|
||||||
.decryptKeyBackupData(
|
|
||||||
keyBackupData,
|
|
||||||
session.safeSessionId!!,
|
|
||||||
cryptoTestData.roomId,
|
|
||||||
decryption!!
|
|
||||||
)
|
)
|
||||||
|
val keysBackupData = KeysBackupData(
|
||||||
|
HashMap<String, RoomKeysBackupData>().apply {
|
||||||
|
put(cryptoTestData.roomId, roomKeysBackupData)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
extractCurveKeyFromRecoveryKey(keyBackupCreationInfo.recoveryKey)?.also {
|
||||||
|
algorithm.setPrivateKey(it)
|
||||||
|
}
|
||||||
|
val sessionsData = algorithm.decryptSessions(keysBackupData)
|
||||||
|
val sessionData = sessionsData.firstOrNull()
|
||||||
assertNotNull(sessionData)
|
assertNotNull(sessionData)
|
||||||
// - Compare the decrypted megolm key with the original one
|
// - Compare the decrypted megolm key with the original one
|
||||||
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
|
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
|
||||||
@ -330,7 +357,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
|
|
||||||
@ -416,7 +443,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Do an e2e backup to the homeserver with a recovery key
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
@ -476,7 +503,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Do an e2e backup to the homeserver with a recovery key
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
@ -534,7 +561,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Do an e2e backup to the homeserver with a recovery key
|
// - Do an e2e backup to the homeserver with a recovery key
|
||||||
// - And log Alice on a new device
|
// - And log Alice on a new device
|
||||||
@ -576,7 +603,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "Password"
|
val password = "Password"
|
||||||
|
|
||||||
@ -636,7 +663,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "Password"
|
val password = "Password"
|
||||||
val badPassword = "Bad Password"
|
val badPassword = "Bad Password"
|
||||||
@ -677,7 +704,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
|
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
|
||||||
@ -705,7 +732,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
|
|
||||||
@ -761,7 +788,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
val wrongPassword = "passw0rd"
|
val wrongPassword = "passw0rd"
|
||||||
@ -792,7 +819,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val password = "password"
|
val password = "password"
|
||||||
|
|
||||||
@ -821,7 +848,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
|
||||||
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
|
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
|
||||||
@ -847,7 +874,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
@ -892,7 +919,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
@ -967,7 +994,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
@ -1054,7 +1081,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||||
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
|
val keysBackupTestHelper = createKeysBackupTestHelper(testHelper, cryptoTestHelper)
|
||||||
|
|
||||||
// - Create a backup version
|
// - Create a backup version
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
|
||||||
@ -1077,4 +1104,9 @@ class KeysBackupTest : InstrumentedTest {
|
|||||||
|
|
||||||
stateObserver.stopAndCheckStates(null)
|
stateObserver.stopAndCheckStates(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open var keyBackupConfig: KeyBackupConfig? = null
|
||||||
|
|
||||||
|
private fun createKeysBackupTestHelper(testHelper: CommonTestHelper, cryptoTestHelper: CryptoTestHelper) =
|
||||||
|
KeysBackupTestHelper(testHelper, cryptoTestHelper, keyBackupConfig)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeyBackupConfig
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||||
@ -34,7 +35,8 @@ import kotlin.coroutines.resume
|
|||||||
|
|
||||||
internal class KeysBackupTestHelper(
|
internal class KeysBackupTestHelper(
|
||||||
private val testHelper: CommonTestHelper,
|
private val testHelper: CommonTestHelper,
|
||||||
private val cryptoTestHelper: CryptoTestHelper
|
private val cryptoTestHelper: CryptoTestHelper,
|
||||||
|
private val keyBackupConfig: KeyBackupConfig? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun waitForKeybackUpBatching() {
|
fun waitForKeybackUpBatching() {
|
||||||
@ -55,6 +57,9 @@ internal class KeysBackupTestHelper(
|
|||||||
|
|
||||||
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||||
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
|
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
|
||||||
|
if (keyBackupConfig != null) {
|
||||||
|
keysBackup.keyBackupConfig = keyBackupConfig
|
||||||
|
}
|
||||||
|
|
||||||
val stateObserver = StateObserver(keysBackup)
|
val stateObserver = StateObserver(keysBackup)
|
||||||
|
|
||||||
@ -81,7 +86,10 @@ internal class KeysBackupTestHelper(
|
|||||||
|
|
||||||
// - Log Alice on a new device
|
// - Log Alice on a new device
|
||||||
val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
|
val aliceSession2 = testHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
|
||||||
|
if (keyBackupConfig != null) {
|
||||||
|
aliceSession2.cryptoService().keysBackupService().keyBackupConfig = keyBackupConfig
|
||||||
|
}
|
||||||
|
aliceSession2.cryptoService().keysBackupService().checkAndStartKeysBackup()
|
||||||
// Test check: aliceSession2 has no keys at login
|
// Test check: aliceSession2 has no keys at login
|
||||||
Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
|
Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
|
||||||
|
|
||||||
@ -105,7 +113,7 @@ internal class KeysBackupTestHelper(
|
|||||||
val stateObserver = StateObserver(keysBackup)
|
val stateObserver = StateObserver(keysBackup)
|
||||||
|
|
||||||
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
|
||||||
keysBackup.prepareKeysBackupVersion(password, null, it)
|
keysBackup.prepareKeysBackupVersion(password, null, null, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.assertNotNull(megolmBackupCreationInfo)
|
Assert.assertNotNull(megolmBackupCreationInfo)
|
||||||
@ -116,6 +124,7 @@ internal class KeysBackupTestHelper(
|
|||||||
val keysVersion = testHelper.waitForCallback<KeysVersion> {
|
val keysVersion = testHelper.waitForCallback<KeysVersion> {
|
||||||
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||||
}
|
}
|
||||||
|
keysBackup.saveBackupRecoveryKey(megolmBackupCreationInfo.recoveryKey, version = keysVersion.version)
|
||||||
|
|
||||||
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
|
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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 atcle
|
||||||
|
*
|
||||||
|
* 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 androidx.test.filters.LargeTest
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeyBackupConfig
|
||||||
|
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
@LargeTest
|
||||||
|
class SymmetricKeysBackupTest : KeysBackupTest() {
|
||||||
|
|
||||||
|
override var keyBackupConfig: KeyBackupConfig? = KeyBackupConfig(
|
||||||
|
defaultAlgorithm = MXCRYPTO_ALGORITHM_AES_256_BACKUP,
|
||||||
|
supportedAlgorithms = listOf(MXCRYPTO_ALGORITHM_AES_256_BACKUP)
|
||||||
|
)
|
||||||
|
}
|
@ -27,9 +27,15 @@ const val MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2"
|
|||||||
const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
|
const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matrix algorithm value for megolm keys backup.
|
* Matrix algorithm value for CURVE_25519 megolm keys backup.
|
||||||
*/
|
*/
|
||||||
const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
|
const val MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix algorithm value for AES-256 megolm keys backup.
|
||||||
|
* Symmetric megolm backup
|
||||||
|
*/
|
||||||
|
const val MXCRYPTO_ALGORITHM_AES_256_BACKUP = "org.matrix.msc3270.v1.aes-hmac-sha2"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Secured Shared Storage algorithm constant.
|
* Secured Shared Storage algorithm constant.
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||||
|
|
||||||
|
data class KeyBackupConfig(
|
||||||
|
val defaultAlgorithm: String = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP,
|
||||||
|
val supportedAlgorithms: List<String> = listOf(MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP)
|
||||||
|
) {
|
||||||
|
fun isAlgorithmSupported(alg: String) = supportedAlgorithms.contains(alg)
|
||||||
|
}
|
@ -22,6 +22,13 @@ import org.matrix.android.sdk.api.listeners.StepProgressListener
|
|||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
|
|
||||||
interface KeysBackupService {
|
interface KeysBackupService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the backup internal configuration, supported algorithm, default algorithm.
|
||||||
|
* @param keyBackupConfig the config
|
||||||
|
*/
|
||||||
|
var keyBackupConfig: KeyBackupConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the current version of the backup from the homeserver.
|
* Retrieve the current version of the backup from the homeserver.
|
||||||
*
|
*
|
||||||
@ -124,6 +131,7 @@ interface KeysBackupService {
|
|||||||
*/
|
*/
|
||||||
fun prepareKeysBackupVersion(
|
fun prepareKeysBackupVersion(
|
||||||
password: String?,
|
password: String?,
|
||||||
|
algorithm: String? = null,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>
|
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||||
)
|
)
|
||||||
|
@ -24,14 +24,16 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorith
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class KeysVersionResult(
|
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")
|
@Json(name = "algorithm")
|
||||||
override val algorithm: String,
|
override val algorithm: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2".
|
* 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")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict,
|
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
|
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.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
|
||||||
|
|
||||||
/**
|
sealed interface MegolmBackupAuthData {
|
||||||
* Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case
|
val privateKeySalt: String?
|
||||||
* of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
|
val privateKeyIterations: Int?
|
||||||
*/
|
val signatures: Map<String, Map<String, String>>?
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MegolmBackupAuthData(
|
|
||||||
/**
|
|
||||||
* The curve25519 public key used to encrypt the backups.
|
|
||||||
*/
|
|
||||||
@Json(name = "public_key")
|
|
||||||
val publicKey: String,
|
|
||||||
|
|
||||||
/**
|
fun isValid(): Boolean
|
||||||
* 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 toJsonDict(): JsonDict
|
||||||
* In case of a backup created from a password, the number of key derivations.
|
fun copy(newSignatures: Map<String, Map<String, String>>?): MegolmBackupAuthData
|
||||||
*/
|
|
||||||
@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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
|
||||||
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
||||||
?.let { Uri.parse(it) }
|
?.let { Uri.parse(it) }
|
||||||
?.takeIf { validateUri(it, homeServerConnectionConfig) }
|
// ?.takeIf { validateUri(it, homeServerConnectionConfig) }
|
||||||
|
|
||||||
private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
|
private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
|
||||||
// Validate the URL, if the configuration is wrong server side, do not override
|
// 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
|
// When this feature lands in spec name = shared_history should be used
|
||||||
@Json(name = "org.matrix.msc3061.shared_history")
|
@Json(name = "org.matrix.msc3061.shared_history")
|
||||||
val sharedHistory: Boolean = false,
|
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
|
package org.matrix.android.sdk.internal.crypto.keysbackup
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
@ -27,14 +26,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
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.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeyBackupConfig
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
@ -50,15 +50,16 @@ 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.extractCurveKeyFromRecoveryKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
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.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.awaitCallback
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
|
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
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.ObjectSigner
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
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.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.BackupKeysResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
||||||
@ -77,7 +78,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.model.MXInboundMegolmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
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.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.di.UserId
|
||||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
@ -87,9 +87,6 @@ import org.matrix.android.sdk.internal.task.TaskThread
|
|||||||
import org.matrix.android.sdk.internal.task.configureWith
|
import org.matrix.android.sdk.internal.task.configureWith
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
import org.matrix.olm.OlmException
|
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 timber.log.Timber
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -99,6 +96,9 @@ import kotlin.random.Random
|
|||||||
* A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys)
|
* A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys)
|
||||||
* to the user's homeserver.
|
* to the user's homeserver.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
private const val DEFAULT_ALGORITHM = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class DefaultKeysBackupService @Inject constructor(
|
internal class DefaultKeysBackupService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@ -121,22 +121,25 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||||
// Task executor
|
// Task executor
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val matrixConfiguration: MatrixConfiguration,
|
private val algorithmFactory: KeysBackupAlgorithmFactory,
|
||||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
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 {
|
) : KeysBackupService {
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
override var keyBackupConfig = KeyBackupConfig(
|
||||||
|
defaultAlgorithm = MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP,
|
||||||
private val keysBackupStateManager = KeysBackupStateManager(uiHandler)
|
supportedAlgorithms = listOf(MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP)
|
||||||
|
)
|
||||||
|
|
||||||
// The backup version
|
// The backup version
|
||||||
override var keysBackupVersion: KeysVersionResult? = null
|
override var keysBackupVersion: KeysVersionResult? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// The backup key being used.
|
private var algorithm: KeysBackupAlgorithm? = null
|
||||||
private var backupOlmPkEncryption: OlmPkEncryption? = null
|
|
||||||
|
|
||||||
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
|
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
|
||||||
|
|
||||||
@ -158,79 +161,18 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
override fun prepareKeysBackupVersion(
|
override fun prepareKeysBackupVersion(
|
||||||
password: String?,
|
password: String?,
|
||||||
|
algorithm: String?,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>
|
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||||
) {
|
) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
cryptoCoroutineScope.launch {
|
||||||
try {
|
prepareKeysBackup(
|
||||||
val olmPkDecryption = OlmPkDecryption()
|
algorithm = algorithm ?: keyBackupConfig.defaultAlgorithm,
|
||||||
val signalableMegolmBackupAuthData = if (password != null) {
|
password = password,
|
||||||
// Generate a private key from the password
|
progressListener = progressListener,
|
||||||
val backgroundProgressListener = if (progressListener == null) {
|
config = keyBackupConfig,
|
||||||
null
|
callback = callback
|
||||||
} 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_MEGOLM_BACKUP,
|
|
||||||
authData = signedMegolmBackupAuthData,
|
|
||||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
|
||||||
)
|
|
||||||
uiHandler.post {
|
|
||||||
callback.onSuccess(creationInfo)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
uiHandler.post {
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,6 +180,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
keysBackupCreationInfo: MegolmBackupCreationInfo,
|
keysBackupCreationInfo: MegolmBackupCreationInfo,
|
||||||
callback: MatrixCallback<KeysVersion>
|
callback: MatrixCallback<KeysVersion>
|
||||||
) {
|
) {
|
||||||
|
if (!keyBackupConfig.isAlgorithmSupported(keysBackupCreationInfo.algorithm)) return Unit.also {
|
||||||
|
callback.onFailure(IllegalArgumentException("Unsupported algorithm"))
|
||||||
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
|
val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
|
||||||
algorithm = keysBackupCreationInfo.algorithm,
|
algorithm = keysBackupCreationInfo.algorithm,
|
||||||
@ -352,7 +297,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<Unit>?
|
callback: MatrixCallback<Unit>?
|
||||||
) {
|
) {
|
||||||
if (!isEnabled() || backupOlmPkEncryption == null || keysBackupVersion == null) {
|
if (!isEnabled() || algorithm == null || keysBackupVersion == null) {
|
||||||
callback?.onFailure(Throwable("Backup not enabled"))
|
callback?.onFailure(Throwable("Backup not enabled"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -430,12 +375,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
||||||
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
||||||
|
|
||||||
if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
|
if (!keyBackupConfig.isAlgorithmSupported(keysBackupVersion.algorithm)) {
|
||||||
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
|
||||||
return KeysBackupVersionTrust(usable = false)
|
return KeysBackupVersionTrust(usable = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mySigs = authData.signatures[userId]
|
if (authData == null || authData.signatures.isNullOrEmpty()) {
|
||||||
|
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
||||||
|
return KeysBackupVersionTrust(usable = false)
|
||||||
|
}
|
||||||
|
val signatures = authData.signatures!!
|
||||||
|
val mySigs = authData.signatures?.get(userId)
|
||||||
if (mySigs.isNullOrEmpty()) {
|
if (mySigs.isNullOrEmpty()) {
|
||||||
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
||||||
return KeysBackupVersionTrust(usable = false)
|
return KeysBackupVersionTrust(usable = false)
|
||||||
@ -459,7 +408,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
var isSignatureValid = false
|
var isSignatureValid = false
|
||||||
try {
|
try {
|
||||||
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures)
|
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.toSignalableJsonDict(), signatures)
|
||||||
isSignatureValid = true
|
isSignatureValid = true
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK")
|
Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK")
|
||||||
@ -485,7 +434,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
val fingerprint = device.fingerprint()
|
val fingerprint = device.fingerprint()
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
try {
|
try {
|
||||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
olmDevice.verifySignature(fingerprint, authData.toSignalableJsonDict(), mySignature)
|
||||||
isSignatureValid = true
|
isSignatureValid = true
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
||||||
@ -519,8 +468,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
|
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
|
if (!keyBackupConfig.isAlgorithmSupported(keysBackupVersion.algorithm)) {
|
||||||
|
Timber.w("trustKeyBackupVersion:trust unsupported algorithm ${keysBackupVersion.algorithm}")
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get auth data to update it
|
// Get auth data to update it
|
||||||
val authData = getMegolmBackupAuthData(keysBackupVersion)
|
val authData = keysBackupVersion.getValidAuthData()
|
||||||
|
|
||||||
if (authData == null) {
|
if (authData == null) {
|
||||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||||
@ -535,7 +492,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
if (trust) {
|
if (trust) {
|
||||||
// Add current device signature
|
// 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)
|
val deviceSignatures = objectSigner.signObject(canonicalJson)
|
||||||
|
|
||||||
@ -548,14 +505,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create an updated version of KeysVersionResult
|
// Create an updated version of KeysVersionResult
|
||||||
val newMegolmBackupAuthData = authData.copy()
|
val newSignatures = authData.signatures.orEmpty().toMutableMap()
|
||||||
|
|
||||||
val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap()
|
|
||||||
newSignatures[userId] = myUserSignatures
|
newSignatures[userId] = myUserSignatures
|
||||||
|
|
||||||
val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy(
|
val newMegolmBackupAuthDataWithNewSignature = authData.copy(newSignatures)
|
||||||
signatures = newSignatures
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
UpdateKeysBackupVersionBody(
|
UpdateKeysBackupVersionBody(
|
||||||
@ -666,36 +619,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() {
|
private fun resetBackupAllGroupSessionsListeners() {
|
||||||
backupAllGroupSessionsCallback = null
|
backupAllGroupSessionsCallback = null
|
||||||
|
|
||||||
@ -721,54 +644,33 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
stepProgressListener: StepProgressListener?,
|
stepProgressListener: StepProgressListener?,
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>
|
callback: MatrixCallback<ImportRoomKeysResult>
|
||||||
) {
|
) {
|
||||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version} alg:${keysVersionResult.algorithm}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val decryption = withContext(coroutineDispatchers.computation) {
|
if (!keyBackupConfig.isAlgorithmSupported(keysVersionResult.algorithm)) {
|
||||||
// Check if the recovery is valid before going any further
|
throw IllegalArgumentException("Unsupported algorithm")
|
||||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
}
|
||||||
|
val backupAlgorithm = algorithmFactory.create(keysVersionResult)
|
||||||
|
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
||||||
|
if (privateKey == null || !backupAlgorithm.keyMatches(privateKey)) {
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||||
throw InvalidParameterException("Invalid recovery key")
|
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 for next time and for gossiping
|
||||||
// Save now as it's valid, don't wait for the import as it could take long.
|
// Save now as it's valid, don't wait for the import as it could take long.
|
||||||
|
backupAlgorithm.setPrivateKey(privateKey)
|
||||||
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
||||||
|
|
||||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||||
|
|
||||||
// Get backed up keys from the homeserver
|
// Get backed up keys from the homeserver
|
||||||
val data = getKeys(sessionId, roomId, keysVersionResult.version)
|
val data = getKeys(sessionId, roomId, keysVersionResult.version)
|
||||||
|
extractCurveKeyFromRecoveryKey(recoveryKey)?.also { privateKey ->
|
||||||
withContext(coroutineDispatchers.computation) {
|
backupAlgorithm.setPrivateKey(privateKey)
|
||||||
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) {
|
||||||
|
backupAlgorithm.decryptSessions(data)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Timber.v(
|
|
||||||
"restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
|
|
||||||
" of $sessionsFromHsCount from the backup store on the homeserver"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Do not trigger a backup for them if they come from the backup version we are using
|
// Do not trigger a backup for them if they come from the backup version we are using
|
||||||
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
||||||
if (backUp) {
|
if (backUp) {
|
||||||
@ -797,7 +699,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
maybeBackupKeys()
|
maybeBackupKeys()
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
|
||||||
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||||
uiHandler.post {
|
uiHandler.post {
|
||||||
@ -902,26 +803,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.
|
* Do a backup if there are new keys, with a delay.
|
||||||
*/
|
*/
|
||||||
@ -1103,15 +984,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Extract MegolmBackupAuthData data from a backup version.
|
* Extract MegolmBackupAuthData data from a backup version.
|
||||||
*
|
*
|
||||||
* @param keysBackupData the key backup data
|
|
||||||
*
|
|
||||||
* @return the authentication if found and valid, null in other case
|
* @return the authentication if found and valid, null in other case
|
||||||
*/
|
*/
|
||||||
private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? {
|
private fun KeysVersionResult.getValidAuthData(): MegolmBackupAuthData? {
|
||||||
return keysBackupData
|
return getAuthDataAsMegolmBackupAuthData()
|
||||||
.takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
|
?.takeIf { it.isValid() }
|
||||||
?.getAuthDataAsMegolmBackupAuthData()
|
|
||||||
?.takeIf { it.publicKey.isNotEmpty() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1125,7 +1002,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? {
|
private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? {
|
||||||
val authData = getMegolmBackupAuthData(keysBackupData)
|
val authData = keysBackupData.getValidAuthData()
|
||||||
|
|
||||||
if (authData == null) {
|
if (authData == null) {
|
||||||
Timber.w("recoveryKeyFromPassword: invalid parameter")
|
Timber.w("recoveryKeyFromPassword: invalid parameter")
|
||||||
@ -1139,8 +1016,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val salt = authData.privateKeySalt!!
|
||||||
|
val iterations = authData.privateKeyIterations!!
|
||||||
// Extract the recovery key from the passphrase
|
// 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)
|
return computeRecoveryKey(data)
|
||||||
}
|
}
|
||||||
@ -1155,32 +1034,20 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean {
|
private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean {
|
||||||
// Build PK decryption instance with the recovery key
|
return try {
|
||||||
val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey)
|
if (!keyBackupConfig.isAlgorithmSupported(keysBackupData.algorithm)) {
|
||||||
|
Timber.w("isValidRecoveryKeyForKeysBackupVersion: unsupported algorithm ${keysBackupData.algorithm}")
|
||||||
if (publicKey == null) {
|
|
||||||
Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null")
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
val algorithm = algorithmFactory.create(keysBackupData)
|
||||||
val authData = getMegolmBackupAuthData(keysBackupData)
|
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) ?: return false
|
||||||
|
val isValid = algorithm.keyMatches(privateKey)
|
||||||
if (authData == null) {
|
algorithm.release()
|
||||||
Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data")
|
isValid
|
||||||
|
} catch (failure: Throwable) {
|
||||||
return false
|
Timber.e(failure, "Can't check validity of recoveryKey")
|
||||||
|
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>) {
|
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
|
||||||
@ -1210,7 +1077,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
private fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
|
private fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
|
||||||
val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
|
val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
|
||||||
|
if (!keyBackupConfig.isAlgorithmSupported(keysVersionResult.algorithm)) {
|
||||||
|
Timber.w("enableKeysBackup: unsupported algorithm ${keysVersionResult.algorithm}")
|
||||||
|
keysBackupStateManager.state = KeysBackupState.Disabled
|
||||||
|
return
|
||||||
|
}
|
||||||
if (retrievedMegolmBackupAuthData != null) {
|
if (retrievedMegolmBackupAuthData != null) {
|
||||||
keysBackupVersion = keysVersionResult
|
keysBackupVersion = keysVersionResult
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
@ -1220,11 +1091,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
backupOlmPkEncryption = OlmPkEncryption().apply {
|
algorithm = algorithmFactory.create(keysVersionResult)
|
||||||
setRecipientKey(retrievedMegolmBackupAuthData.publicKey)
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e, "Error while creating algorithm")
|
||||||
} catch (e: OlmException) {
|
|
||||||
Timber.e(e, "OlmException")
|
|
||||||
keysBackupStateManager.state = KeysBackupState.Disabled
|
keysBackupStateManager.state = KeysBackupState.Disabled
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1260,8 +1129,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
cryptoStore.setKeyBackupVersion(null)
|
cryptoStore.setKeyBackupVersion(null)
|
||||||
cryptoStore.setKeysBackupData(null)
|
cryptoStore.setKeysBackupData(null)
|
||||||
backupOlmPkEncryption?.releaseEncryption()
|
algorithm?.release()
|
||||||
backupOlmPkEncryption = null
|
algorithm = null
|
||||||
|
|
||||||
// Reset backup markers
|
// Reset backup markers
|
||||||
cryptoStore.resetBackupMarkers()
|
cryptoStore.resetBackupMarkers()
|
||||||
@ -1275,7 +1144,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
Timber.v("backupKeys")
|
Timber.v("backupKeys")
|
||||||
|
|
||||||
// Sanity check, as this method can be called after a delay, the state may have change during the delay
|
// 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")
|
Timber.v("backupKeys: Invalid configuration")
|
||||||
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
|
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
|
||||||
resetBackupAllGroupSessionsListeners()
|
resetBackupAllGroupSessionsListeners()
|
||||||
@ -1287,6 +1156,12 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
Timber.v("backupKeys: Invalid state: ${getState()}")
|
Timber.v("backupKeys: Invalid state: ${getState()}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val recoveryKey = cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
|
||||||
|
extractCurveKeyFromRecoveryKey(recoveryKey)?.also { privateKey ->
|
||||||
|
// symmetric algorithm need private key to backup :/
|
||||||
|
// a bit hugly, but all this code needs refactoring
|
||||||
|
algorithm?.setPrivateKey(privateKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Get a chunk of keys to backup
|
// Get a chunk of keys to backup
|
||||||
val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT)
|
val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT)
|
||||||
@ -1311,7 +1186,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
// Gather data to send to the homeserver
|
// Gather data to send to the homeserver
|
||||||
// roomId -> sessionId -> MXKeyBackupData
|
// roomId -> sessionId -> MXKeyBackupData
|
||||||
val keysBackupData = KeysBackupData()
|
val keysBackupData = KeysBackupData()
|
||||||
|
val recoveryKey = cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
|
||||||
|
extractCurveKeyFromRecoveryKey(recoveryKey)?.also { privateKey ->
|
||||||
|
algorithm?.setPrivateKey(privateKey)
|
||||||
|
}
|
||||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||||
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
|
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
|
||||||
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
|
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
|
||||||
@ -1411,8 +1289,6 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
// Gather information for each key
|
// Gather information for each key
|
||||||
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey)
|
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
|
val sessionData = inboundGroupSessionStore
|
||||||
.getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
|
.getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
|
||||||
?.let {
|
?.let {
|
||||||
@ -1421,30 +1297,10 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: return null
|
?: 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()
|
val sessionBackupData = tryOrNull {
|
||||||
.adapter(Map::class.java)
|
algorithm?.encryptSession(sessionData)
|
||||||
.toJson(sessionBackupData)
|
} ?: return null
|
||||||
|
|
||||||
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
|
|
||||||
return KeyBackupData(
|
return KeyBackupData(
|
||||||
firstMessageIndex = try {
|
firstMessageIndex = try {
|
||||||
olmInboundGroupSessionWrapper.session.firstKnownIndex
|
olmInboundGroupSessionWrapper.session.firstKnownIndex
|
||||||
@ -1455,11 +1311,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
|
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
|
||||||
isVerified = device?.isVerified == true,
|
isVerified = device?.isVerified == true,
|
||||||
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
|
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
|
||||||
sessionData = mapOf(
|
sessionData = sessionBackupData
|
||||||
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
|
||||||
"mac" to encryptedSessionBackupData.mMac,
|
|
||||||
"ephemeral" to encryptedSessionBackupData.mEphemeralKey
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1471,43 +1323,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
return sessionData.sharedHistory
|
return sessionData.sharedHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
private fun getPrivateKey(): ByteArray {
|
||||||
@WorkerThread
|
return byteArrayOf()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
@ -1559,3 +1376,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
|
|
||||||
override fun toString() = "KeysBackup for $userId"
|
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.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||||
import timber.log.Timber
|
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>()
|
private val listeners = ArrayList<KeysBackupStateListener>()
|
||||||
|
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.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_AES_256_BACKUP
|
||||||
|
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.KeyBackupConfig
|
||||||
|
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.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.api.util.toBase64NoPadding
|
||||||
|
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
||||||
|
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
|
||||||
|
import org.matrix.android.sdk.internal.crypto.tools.AesHmacSha2
|
||||||
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
|
import org.matrix.olm.OlmPkDecryption
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.security.SecureRandom
|
||||||
|
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?,
|
||||||
|
config: KeyBackupConfig,
|
||||||
|
callback: MatrixCallback<MegolmBackupCreationInfo>
|
||||||
|
) = withContext(coroutineDispatchers.io) {
|
||||||
|
if (!config.isAlgorithmSupported(algorithm)) return@withContext Unit.also {
|
||||||
|
callback.onFailure(IllegalArgumentException("Unsupported algorithm"))
|
||||||
|
}
|
||||||
|
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 megolmBackupAuthData = 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)
|
||||||
|
MegolmBackupCurve25519AuthData(
|
||||||
|
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||||
|
privateKeySalt = generatePrivateKeyResult.salt,
|
||||||
|
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val publicKey = olmPkDecryption.generateKey()
|
||||||
|
MegolmBackupCurve25519AuthData(
|
||||||
|
publicKey = publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val signatures = signKeyBackup(megolmBackupAuthData)
|
||||||
|
|
||||||
|
val signedMegolmBackupCurve25519AuthData = megolmBackupAuthData.copy(
|
||||||
|
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 signKeyBackup(authData: MegolmBackupAuthData): MutableMap<String, MutableMap<String, String>> {
|
||||||
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.toSignalableJsonDict())
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
return signatures
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareAES(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||||
|
try {
|
||||||
|
val privateKey: ByteArray
|
||||||
|
val authData: MegolmBackupAes256AuthData
|
||||||
|
if (password == null) {
|
||||||
|
privateKey = ByteArray(32).also {
|
||||||
|
SecureRandom().nextBytes(it)
|
||||||
|
}
|
||||||
|
authData = MegolmBackupAes256AuthData()
|
||||||
|
} else {
|
||||||
|
val result = generatePrivateKeyWithPassword(password, progressListener)
|
||||||
|
privateKey = result.privateKey
|
||||||
|
authData = MegolmBackupAes256AuthData(
|
||||||
|
privateKeySalt = result.salt,
|
||||||
|
privateKeyIterations = result.iterations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val encInfo = AesHmacSha2.calculateKeyCheck(privateKey, null)
|
||||||
|
val authDataWithKeyCheck = authData.copy(
|
||||||
|
iv = encInfo.initializationVector.toBase64NoPadding(),
|
||||||
|
mac = encInfo.mac.toBase64NoPadding()
|
||||||
|
)
|
||||||
|
|
||||||
|
val signatures = signKeyBackup(authDataWithKeyCheck)
|
||||||
|
val creationInfo = MegolmBackupCreationInfo(
|
||||||
|
algorithm = MXCRYPTO_ALGORITHM_AES_256_BACKUP,
|
||||||
|
authData = authDataWithKeyCheck.copy(
|
||||||
|
signatures = signatures
|
||||||
|
),
|
||||||
|
recoveryKey = computeRecoveryKey(privateKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onSuccess(creationInfo)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.internal.crypto.keysbackup.algorithm
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_AES_256_BACKUP
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||||
|
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.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
|
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.crypto.tools.AesHmacSha2
|
||||||
|
import org.matrix.olm.OlmException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.security.InvalidParameterException
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
internal class KeysBackupAes256Algorithm(keysVersions: KeysVersionResult) : KeysBackupAlgorithm {
|
||||||
|
|
||||||
|
override val untrusted: Boolean = false
|
||||||
|
|
||||||
|
private val aesAuthData: MegolmBackupAes256AuthData
|
||||||
|
private var privateKey: ByteArray? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (keysVersions.algorithm != MXCRYPTO_ALGORITHM_AES_256_BACKUP) {
|
||||||
|
throw IllegalStateException("Algorithm doesn't match")
|
||||||
|
}
|
||||||
|
aesAuthData = keysVersions.getAuthDataAsMegolmBackupAuthData() as MegolmBackupAes256AuthData
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPrivateKey(privateKey: ByteArray) {
|
||||||
|
this.privateKey = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
|
||||||
|
val privateKey = privateKey
|
||||||
|
if (privateKey == null || !keyMatches(privateKey)) {
|
||||||
|
Timber.e("Key does not match")
|
||||||
|
throw IllegalStateException("Key does not match")
|
||||||
|
}
|
||||||
|
val encryptedSessionBackupData = try {
|
||||||
|
val sessionDataJson = sessionData.asBackupJson()
|
||||||
|
AesHmacSha2.encrypt(privateKey = privateKey, secretName = sessionData.sessionId ?: "", sessionDataJson)
|
||||||
|
} catch (e: OlmException) {
|
||||||
|
Timber.e(e, "Error while encrypting backup data.")
|
||||||
|
null
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"ciphertext" to encryptedSessionBackupData.cipherRawBytes.toBase64NoPadding(),
|
||||||
|
"mac" to encryptedSessionBackupData.mac.toBase64NoPadding(),
|
||||||
|
"iv" to encryptedSessionBackupData.initializationVector.toBase64NoPadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decryptSessions(data: KeysBackupData): List<MegolmSessionData> {
|
||||||
|
val privateKey = privateKey
|
||||||
|
if (privateKey == null || !keyMatches(privateKey)) {
|
||||||
|
Timber.e("Invalid recovery key for this keys version")
|
||||||
|
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, privateKey)
|
||||||
|
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, privateKey: ByteArray): MegolmSessionData? {
|
||||||
|
val cipherRawBytes = sessionData["ciphertext"]?.toString()?.fromBase64() ?: return null
|
||||||
|
val mac = sessionData["mac"]?.toString()?.fromBase64() ?: throw IllegalStateException("Bad mac")
|
||||||
|
val iv = sessionData["iv"]?.toString()?.fromBase64() ?: ByteArray(16)
|
||||||
|
|
||||||
|
val encryptionInfo = AesHmacSha2.EncryptionInfo(
|
||||||
|
cipherRawBytes = cipherRawBytes,
|
||||||
|
mac = mac,
|
||||||
|
initializationVector = iv
|
||||||
|
)
|
||||||
|
return try {
|
||||||
|
val decrypted = AesHmacSha2.decrypt(privateKey, sessionId, encryptionInfo)
|
||||||
|
createMegolmSessionData(decrypted, sessionId, roomId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Exception while decrypting")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
privateKey?.apply {
|
||||||
|
Arrays.fill(this, 0)
|
||||||
|
}
|
||||||
|
privateKey = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override val authData: MegolmBackupAuthData = aesAuthData
|
||||||
|
|
||||||
|
override fun keyMatches(privateKey: ByteArray): Boolean {
|
||||||
|
return if (aesAuthData.mac != null) {
|
||||||
|
val keyCheckMac = AesHmacSha2.calculateKeyCheck(privateKey, aesAuthData.iv).mac
|
||||||
|
val authDataMac = aesAuthData.mac.fromBase64()
|
||||||
|
return keyCheckMac.contentEquals(authDataMac)
|
||||||
|
} else {
|
||||||
|
// if we have no information, we have to assume the key is right
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.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
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
|
||||||
|
internal interface KeysBackupAlgorithm {
|
||||||
|
|
||||||
|
val authData: MegolmBackupAuthData
|
||||||
|
val untrusted: Boolean
|
||||||
|
|
||||||
|
fun setPrivateKey(privateKey: ByteArray)
|
||||||
|
fun encryptSession(sessionData: MegolmSessionData): JsonDict?
|
||||||
|
fun decryptSessions(data: KeysBackupData): List<MegolmSessionData>
|
||||||
|
fun keyMatches(privateKey: ByteArray): Boolean
|
||||||
|
fun release()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun KeysBackupAlgorithm.createMegolmSessionData(decrypted: String, sessionId: String, roomId: String): MegolmSessionData? {
|
||||||
|
val moshi = MoshiProvider.providesMoshi()
|
||||||
|
val adapter = moshi.adapter(MegolmSessionData::class.java)
|
||||||
|
val sessionBackupData: MegolmSessionData = adapter.fromJson(decrypted) ?: return null
|
||||||
|
return sessionBackupData.copy(
|
||||||
|
sessionId = sessionId,
|
||||||
|
roomId = roomId,
|
||||||
|
untrusted = untrusted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun MegolmSessionData.asBackupJson(): String {
|
||||||
|
val sessionBackupData = mapOf(
|
||||||
|
"algorithm" to algorithm,
|
||||||
|
"sender_key" to senderKey,
|
||||||
|
"sender_claimed_keys" to senderClaimedKeys,
|
||||||
|
"forwarding_curve25519_key_chain" to (forwardingCurve25519KeyChain.orEmpty()),
|
||||||
|
"session_key" to sessionKey,
|
||||||
|
"org.matrix.msc3061.shared_history" to sharedHistory,
|
||||||
|
"untrusted" to untrusted
|
||||||
|
)
|
||||||
|
return MoshiProvider.providesMoshi()
|
||||||
|
.adapter(Map::class.java)
|
||||||
|
.toJson(sessionBackupData)
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.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 -> {
|
||||||
|
KeysBackupAes256Algorithm(keysVersion)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalStateException("Unknown algorithm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.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.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||||
|
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
|
||||||
|
private var privateKey: ByteArray? = null
|
||||||
|
|
||||||
|
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 setPrivateKey(privateKey: ByteArray) {
|
||||||
|
this.privateKey = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encryptSession(sessionData: MegolmSessionData): JsonDict? {
|
||||||
|
val encryptedSessionBackupData = try {
|
||||||
|
val sessionDataJson = sessionData.asBackupJson()
|
||||||
|
encryptionKey.encrypt(sessionDataJson)
|
||||||
|
} catch (e: OlmException) {
|
||||||
|
Timber.e(e, "Error while encrypting backup data.")
|
||||||
|
throw e
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
||||||
|
"mac" to encryptedSessionBackupData.mMac,
|
||||||
|
"ephemeral" to encryptedSessionBackupData.mEphemeralKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decryptSessions(data: KeysBackupData): List<MegolmSessionData> {
|
||||||
|
fun pkDecryptionFromPrivateKey(privateKey: ByteArray): OlmPkDecryption? {
|
||||||
|
var decryption: OlmPkDecryption? = null
|
||||||
|
try {
|
||||||
|
decryption = OlmPkDecryption()
|
||||||
|
decryption.setPrivateKey(privateKey)
|
||||||
|
} catch (e: OlmException) {
|
||||||
|
Timber.e(e, "OlmException")
|
||||||
|
}
|
||||||
|
return decryption
|
||||||
|
}
|
||||||
|
|
||||||
|
val privateKey = this.privateKey
|
||||||
|
if (privateKey == null || !keyMatches(privateKey)) {
|
||||||
|
Timber.e("Invalid recovery key for this keys version")
|
||||||
|
throw InvalidParameterException("Invalid recovery key")
|
||||||
|
}
|
||||||
|
// Get a PK decryption instance
|
||||||
|
val decryption = pkDecryptionFromPrivateKey(privateKey)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decryption.releaseDecryption()
|
||||||
|
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? {
|
||||||
|
val ciphertext = sessionData["ciphertext"]?.toString()
|
||||||
|
val mac = sessionData["mac"]?.toString()
|
||||||
|
val ephemeralKey = sessionData["ephemeral"]?.toString()
|
||||||
|
|
||||||
|
return 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)
|
||||||
|
createMegolmSessionData(decrypted, sessionId, roomId)
|
||||||
|
} catch (e: OlmException) {
|
||||||
|
Timber.e(e, "Exception while decrypting")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
encryptionKey.releaseEncryption()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val authData: MegolmBackupAuthData = curveAuthData
|
||||||
|
|
||||||
|
override fun keyMatches(privateKey: ByteArray): Boolean {
|
||||||
|
fun pkPublicKeyFromPrivateKey(privateKey: ByteArray): String? {
|
||||||
|
// 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 = pkPublicKeyFromPrivateKey(privateKey)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.internal.crypto.keysbackup.model
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
|
|
||||||
internal data class SignalableMegolmBackupAuthData(
|
|
||||||
val publicKey: String,
|
|
||||||
val privateKeySalt: String? = null,
|
|
||||||
val privateKeyIterations: Int? = null
|
|
||||||
) {
|
|
||||||
fun signalableJSONDictionary(): JsonDict = HashMap<String, Any>().apply {
|
|
||||||
put("public_key", publicKey)
|
|
||||||
|
|
||||||
privateKeySalt?.let {
|
|
||||||
put("private_key_salt", it)
|
|
||||||
}
|
|
||||||
privateKeyIterations?.let {
|
|
||||||
put("private_key_iterations", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,7 +23,9 @@ import org.matrix.android.sdk.api.util.JsonDict
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class CreateKeysBackupVersionBody(
|
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")
|
@Json(name = "algorithm")
|
||||||
override val algorithm: String,
|
override val algorithm: String,
|
||||||
|
@ -16,8 +16,11 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
|
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
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.MegolmBackupAuthData
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCurve25519AuthData
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
|
||||||
@ -41,12 +44,14 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
|
|||||||
internal interface KeysAlgorithmAndData {
|
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
|
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
|
val authData: JsonDict
|
||||||
|
|
||||||
@ -54,9 +59,12 @@ internal interface KeysAlgorithmAndData {
|
|||||||
* Facility method to convert authData to a MegolmBackupAuthData object.
|
* Facility method to convert authData to a MegolmBackupAuthData object.
|
||||||
*/
|
*/
|
||||||
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
|
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
|
||||||
return MoshiProvider.providesMoshi()
|
val moshi = MoshiProvider.providesMoshi()
|
||||||
.takeIf { algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
|
val moshiAdapter = when (algorithm) {
|
||||||
?.adapter(MegolmBackupAuthData::class.java)
|
MXCRYPTO_ALGORITHM_CURVE_25519_BACKUP -> moshi.adapter(MegolmBackupCurve25519AuthData::class.java)
|
||||||
?.fromJsonValue(authData)
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class UpdateKeysBackupVersionBody(
|
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")
|
@Json(name = "algorithm")
|
||||||
override val algorithm: String,
|
override val algorithm: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2".
|
* 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")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict,
|
override val authData: JsonDict,
|
||||||
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||||
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
|
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||||
@ -43,17 +42,12 @@ import org.matrix.android.sdk.api.util.fromBase64
|
|||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
import org.matrix.android.sdk.internal.crypto.tools.AesHmacSha2
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.olm.OlmPkMessage
|
import org.matrix.olm.OlmPkMessage
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@ -214,84 +208,25 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||||||
@Throws
|
@Throws
|
||||||
private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent {
|
private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent {
|
||||||
secretKey as RawBytesKeySpec
|
secretKey as RawBytesKeySpec
|
||||||
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
val result = AesHmacSha2.encrypt(secretKey.privateKey, secretName, clearDataBase64)
|
||||||
secretKey.privateKey,
|
|
||||||
ByteArray(32) { 0.toByte() },
|
|
||||||
secretName.toByteArray(),
|
|
||||||
64
|
|
||||||
)
|
|
||||||
|
|
||||||
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
|
||||||
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
|
||||||
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
|
||||||
|
|
||||||
val secureRandom = SecureRandom()
|
|
||||||
val iv = ByteArray(16)
|
|
||||||
secureRandom.nextBytes(iv)
|
|
||||||
|
|
||||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
|
||||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
|
||||||
// of a single bit of salt is a price we have to pay.
|
|
||||||
iv[9] = iv[9] and 0x7f
|
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
|
||||||
|
|
||||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
|
||||||
val ivParameterSpec = IvParameterSpec(iv)
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
|
||||||
// secret are not that big, just do Final
|
|
||||||
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
|
|
||||||
require(cipherBytes.isNotEmpty())
|
|
||||||
|
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
|
||||||
mac.init(macKeySpec)
|
|
||||||
val digest = mac.doFinal(cipherBytes)
|
|
||||||
|
|
||||||
return EncryptedSecretContent(
|
return EncryptedSecretContent(
|
||||||
ciphertext = cipherBytes.toBase64NoPadding(),
|
ciphertext = result.cipherRawBytes.toBase64NoPadding(),
|
||||||
initializationVector = iv.toBase64NoPadding(),
|
initializationVector = result.initializationVector.toBase64NoPadding(),
|
||||||
mac = digest.toBase64NoPadding()
|
mac = result.mac.toBase64NoPadding()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String {
|
private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String {
|
||||||
secretKey as RawBytesKeySpec
|
secretKey as RawBytesKeySpec
|
||||||
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
|
||||||
secretKey.privateKey,
|
|
||||||
ByteArray(32) { 0.toByte() },
|
|
||||||
secretName.toByteArray(),
|
|
||||||
64
|
|
||||||
)
|
|
||||||
|
|
||||||
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
|
||||||
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
|
||||||
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
|
||||||
|
|
||||||
val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
|
val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
|
||||||
|
val cipher = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
||||||
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
|
val mac = cipherContent.mac?.fromBase64() ?: throw SharedSecretStorageError.BadMac
|
||||||
|
val encryptionInfo = AesHmacSha2.EncryptionInfo(
|
||||||
// Check Signature
|
cipherRawBytes = cipher,
|
||||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
initializationVector = iv,
|
||||||
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
mac = mac
|
||||||
val digest = mac.doFinal(cipherRawBytes)
|
)
|
||||||
|
return AesHmacSha2.decrypt(secretKey.privateKey, secretName, encryptionInfo)
|
||||||
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
|
|
||||||
throw SharedSecretStorageError.BadMac
|
|
||||||
}
|
|
||||||
|
|
||||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
|
||||||
|
|
||||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
|
||||||
val ivParameterSpec = IvParameterSpec(iv)
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
|
||||||
// secret are not that big, just do Final
|
|
||||||
val decryptedSecret = cipher.doFinal(cipherRawBytes)
|
|
||||||
|
|
||||||
require(decryptedSecret.isNotEmpty())
|
|
||||||
|
|
||||||
return String(decryptedSecret, Charsets.UTF_8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.internal.crypto.tools
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
|
||||||
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.experimental.and
|
||||||
|
|
||||||
|
internal object AesHmacSha2 {
|
||||||
|
|
||||||
|
class EncryptionInfo(
|
||||||
|
val cipherRawBytes: ByteArray,
|
||||||
|
val mac: ByteArray,
|
||||||
|
val initializationVector: ByteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encryption algorithm aes-hmac-sha2
|
||||||
|
* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows:
|
||||||
|
*
|
||||||
|
* Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes
|
||||||
|
* of 0, and with the secret name as the info.
|
||||||
|
*
|
||||||
|
* The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||||
|
*
|
||||||
|
* Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use
|
||||||
|
* this as the AES initialization vector.
|
||||||
|
* This becomes the iv property, encoded using base64.
|
||||||
|
*
|
||||||
|
* Encrypt the data using AES-CTR-256 using the AES key generated above.
|
||||||
|
*
|
||||||
|
* This encrypted data, encoded using base64, becomes the ciphertext property.
|
||||||
|
*
|
||||||
|
* Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
|
||||||
|
* The resulting MAC is base64-encoded and becomes the mac property.
|
||||||
|
* (We use AES-CTR to match file encryption and key exports.)
|
||||||
|
*/
|
||||||
|
@Throws
|
||||||
|
fun encrypt(privateKey: ByteArray, secretName: String, clearDataBase64: String, ivString: String? = null): EncryptionInfo {
|
||||||
|
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
||||||
|
privateKey,
|
||||||
|
zeroByteArray(32),
|
||||||
|
secretName.toByteArray(),
|
||||||
|
64
|
||||||
|
)
|
||||||
|
|
||||||
|
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||||
|
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
||||||
|
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
||||||
|
|
||||||
|
val iv = if (ivString != null) {
|
||||||
|
ivString.fromBase64()
|
||||||
|
} else {
|
||||||
|
val secureRandom = SecureRandom()
|
||||||
|
ByteArray(16).apply {
|
||||||
|
secureRandom.nextBytes(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||||
|
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||||
|
// of a single bit of salt is a price we have to pay.
|
||||||
|
iv[9] = iv[9] and 0x7f
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
|
||||||
|
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||||
|
val ivParameterSpec = IvParameterSpec(iv)
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||||
|
// secret are not that big, just do Final
|
||||||
|
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
|
||||||
|
require(cipherBytes.isNotEmpty())
|
||||||
|
|
||||||
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(macKeySpec)
|
||||||
|
val digest = mac.doFinal(cipherBytes)
|
||||||
|
|
||||||
|
return EncryptionInfo(
|
||||||
|
cipherRawBytes = cipherBytes,
|
||||||
|
initializationVector = iv,
|
||||||
|
mac = digest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(privateKey: ByteArray, secretName: String, aesHmacSha2Result: EncryptionInfo): String {
|
||||||
|
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
||||||
|
privateKey,
|
||||||
|
zeroByteArray(32),
|
||||||
|
secretName.toByteArray(),
|
||||||
|
64
|
||||||
|
)
|
||||||
|
|
||||||
|
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||||
|
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
||||||
|
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
||||||
|
|
||||||
|
val iv = aesHmacSha2Result.initializationVector
|
||||||
|
val cipherRawBytes = aesHmacSha2Result.cipherRawBytes
|
||||||
|
|
||||||
|
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||||
|
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
||||||
|
val digest = mac.doFinal(cipherRawBytes)
|
||||||
|
if (!aesHmacSha2Result.mac.contentEquals(digest)) {
|
||||||
|
throw SharedSecretStorageError.BadMac
|
||||||
|
}
|
||||||
|
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||||
|
|
||||||
|
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||||
|
val ivParameterSpec = IvParameterSpec(iv)
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||||
|
// secret are not that big, just do Final
|
||||||
|
val decryptedSecret = cipher.doFinal(cipherRawBytes)
|
||||||
|
|
||||||
|
require(decryptedSecret.isNotEmpty())
|
||||||
|
|
||||||
|
return String(decryptedSecret, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calculate the MAC for checking the key.
|
||||||
|
*
|
||||||
|
* @param {ByteArray} [key] the key to use
|
||||||
|
* @param {string} [iv] The initialization vector as a base64-encoded string.
|
||||||
|
* If omitted, a random initialization vector will be created.
|
||||||
|
* @return [EncryptionInfo] An object that contains, `mac` and `iv` properties.
|
||||||
|
*/
|
||||||
|
fun calculateKeyCheck(key: ByteArray, iv: String?): EncryptionInfo {
|
||||||
|
val zerosStr = String(zeroByteArray(32))
|
||||||
|
return encrypt(key, "", zerosStr, iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun zeroByteArray(size: Int): ByteArray = ByteArray(size) { 0.toByte() }
|
||||||
|
}
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.os.Handler
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
@ -66,6 +67,8 @@ internal interface MatrixComponent {
|
|||||||
|
|
||||||
fun moshi(): Moshi
|
fun moshi(): Moshi
|
||||||
|
|
||||||
|
fun uiHandler(): Handler
|
||||||
|
|
||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
fun okHttpClient(): OkHttpClient
|
fun okHttpClient(): OkHttpClient
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.di
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.os.Handler
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -25,6 +26,7 @@ import kotlinx.coroutines.android.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.createBackgroundHandler
|
import org.matrix.android.sdk.internal.util.createBackgroundHandler
|
||||||
|
import org.matrix.android.sdk.internal.util.createUIHandler
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@ -51,6 +53,12 @@ internal object MatrixModule {
|
|||||||
return context.resources
|
return context.resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
fun providesUIHandler(): Handler {
|
||||||
|
return createUIHandler()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@CacheDirectory
|
@CacheDirectory
|
||||||
|
@ -144,7 +144,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(
|
|||||||
// for the backup and store it in the 4S
|
// for the backup and store it in the 4S
|
||||||
if (session.sharedSecretStorageService().isRecoverySetup()) {
|
if (session.sharedSecretStorageService().isRecoverySetup()) {
|
||||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||||
}
|
}
|
||||||
pendingBackupCreationInfo = creationInfo
|
pendingBackupCreationInfo = creationInfo
|
||||||
val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()
|
val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()
|
||||||
|
@ -97,7 +97,7 @@ class KeysBackupSetupSharedViewModel @Inject constructor(
|
|||||||
val requestedId = currentRequestId.value!!
|
val requestedId = currentRequestId.value!!
|
||||||
|
|
||||||
mxSession.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase,
|
mxSession.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase,
|
||||||
object : ProgressListener {
|
progressListener = object : ProgressListener {
|
||||||
override fun onProgress(progress: Int, total: Int) {
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
if (requestedId != currentRequestId.value) {
|
if (requestedId != currentRequestId.value) {
|
||||||
// this is an old request, we can't cancel but we can ignore
|
// 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) {
|
override fun onSuccess(data: MegolmBackupCreationInfo) {
|
||||||
if (requestedId != currentRequestId.value) {
|
if (requestedId != currentRequestId.value) {
|
||||||
// this is an old request, we can't cancel but we can ignore
|
// 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")
|
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
|
||||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, null, it)
|
||||||
}
|
}
|
||||||
val version = awaitCallback<KeysVersion> {
|
val version = awaitCallback<KeysVersion> {
|
||||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user