Merge pull request #5939 from vector-im/feature/bca/fix_5906

Desynchronized 4S | Megolm backup causing Unusable backup
This commit is contained in:
Valere 2022-05-13 15:43:18 +02:00 committed by GitHub
commit d40f8b0c56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 699 additions and 280 deletions

1
changelog.d/5906.bugfix Normal file
View file

@ -0,0 +1 @@
Desynchronized 4S | Megolm backup causing Unusable backup

View file

@ -203,17 +203,49 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
// Alice sends a message
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1).first().eventId.let { sentEventId ->
// ensure bob got it
ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
}
// Bob send 3 messages
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
}
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
}
testHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1).first().eventId.let { sentEventId ->
// ensure alice got it
ensureEventReceived(aliceRoomId, sentEventId, aliceSession, true)
}
// Alice sends a message
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
testHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1).first().eventId.let { sentEventId ->
// ensure bob got it
ensureEventReceived(aliceRoomId, sentEventId, bobSession, true)
}
return cryptoTestData
}
private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
if (andCanDecrypt) {
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
} else {
timeLineEvent != null
}
}
}
}
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
assertEquals(EventType.ENCRYPTED, event.type)
assertNotNull(event.content)
@ -381,9 +413,9 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
} != null
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
} != null
}
}
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
@ -411,7 +443,8 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
requestID!!,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!)
bob.sessionParams.credentials.deviceId!!
)
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null

View file

@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@ -37,7 +36,9 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersio
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.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
@ -54,18 +55,16 @@ import java.util.concurrent.CountDownLatch
@LargeTest
class KeysBackupTest : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
private val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
/**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
* - Check backup keys after having marked one as backed up
* - Reset keys backup markers
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun roomKeysTest_testBackupStore_ok() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
// From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
@ -104,6 +103,8 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun prepareKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
assertNotNull(bobSession.cryptoService().keysBackupService())
@ -132,7 +133,11 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun createKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
cryptoTestHelper.initializeCrossSigning(bobSession)
val keysBackup = bobSession.cryptoService().keysBackupService()
@ -147,13 +152,46 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
// Create the version
testHelper.doSync<KeysVersion> {
val version = testHelper.doSync<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
// Backup must be enable now
assertTrue(keysBackup.isEnabled)
// Check that it's signed with MSK
val versionResult = testHelper.doSync<KeysVersionResult?> {
keysBackup.getVersion(version.version, it)
}
val trust = testHelper.doSync<KeysBackupVersionTrust> {
keysBackup.getKeysBackupTrust(versionResult!!, it)
}
assertEquals("Should have 2 signatures", 2, trust.signatures.size)
trust.signatures
.firstOrNull { it is KeysBackupVersionTrustSignature.DeviceSignature }
.let {
assertNotNull("Should be signed by a device", it)
it as KeysBackupVersionTrustSignature.DeviceSignature
}.let {
assertEquals("Should be signed by current device", bobSession.sessionParams.deviceId, it.deviceId)
assertTrue("Signature should be valid", it.valid)
}
trust.signatures
.firstOrNull { it is KeysBackupVersionTrustSignature.UserSignature }
.let {
assertNotNull("Should be signed by a user", it)
it as KeysBackupVersionTrustSignature.UserSignature
}.let {
val msk = bobSession.cryptoService().crossSigningService()
.getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey
assertEquals("Should be signed by my msk 1", msk, it.keyId)
assertEquals("Should be signed by my msk 2", msk, it.cryptoCrossSigningKey?.unpaddedBase64PublicKey)
assertTrue("Signature should be valid", it.valid)
}
stateObserver.stopAndCheckStates(null)
testHelper.signOutAndClose(bobSession)
}
@ -163,8 +201,11 @@ class KeysBackupTest : InstrumentedTest {
* - Check the backup completes
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun backupAfterCreateKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
keysBackupTestHelper.waitForKeybackUpBatching()
@ -204,8 +245,11 @@ class KeysBackupTest : InstrumentedTest {
* Check that backupAllGroupSessions() returns valid data
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun backupAllGroupSessionsTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
@ -249,8 +293,11 @@ class KeysBackupTest : InstrumentedTest {
* - Compare the decrypted megolm key with the original one
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testEncryptAndDecryptKeysBackupData() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService
@ -293,8 +340,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver
@ -378,8 +428,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -438,8 +491,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -496,8 +552,11 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -538,8 +597,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
// - Do an e2e backup to the homeserver with a password
@ -598,8 +660,11 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun trustKeyBackupVersionWithWrongPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
val badPassword = "Bad Password"
@ -639,8 +704,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a wrong recovery key
@ -673,8 +741,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testBackupWithPassword() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
@ -730,8 +801,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun restoreKeysBackupWithAWrongPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
val wrongPassword = "passw0rd"
@ -767,8 +841,11 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
@ -797,8 +874,11 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a password
@ -829,8 +909,11 @@ class KeysBackupTest : InstrumentedTest {
* - Check the returned KeysVersionResult is trusted
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testIsKeysBackupTrusted() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -855,7 +938,7 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(keysBackupVersionTrust.usable)
assertEquals(1, keysBackupVersionTrust.signatures.size)
val signature = keysBackupVersionTrust.signatures[0]
val signature = keysBackupVersionTrust.signatures[0] as KeysBackupVersionTrustSignature.DeviceSignature
assertTrue(signature.valid)
assertNotNull(signature.device)
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
@ -865,66 +948,6 @@ class KeysBackupTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
/**
* Check backup starts automatically if there is an existing and compatible backup
* version on the homeserver.
* - Create a backup version
* - Restart alice session
* -> The new alice session must back up to the same version
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testCheckAndStartKeysBackupWhenRestartingAMatrixSession() {
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
assertFalse(keysBackup.isEnabled)
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled)
// - Restart alice session
// - Log Alice on a new device
val aliceSession2 = testHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
cryptoTestData.cleanUp(testHelper)
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
val stateObserver2 = StateObserver(keysBackup2)
// -> The new alice session must back up to the same version
val latch = CountDownLatch(1)
var count = 0
keysBackup2.addListener(object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
// Check the backup completes
if (newState == KeysBackupState.ReadyToBackUp) {
count++
if (count == 2) {
// Remove itself from the list of listeners
keysBackup2.removeListener(this)
latch.countDown()
}
}
}
})
testHelper.await(latch)
assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion)
stateObserver.stopAndCheckStates(null)
stateObserver2.stopAndCheckStates(null)
testHelper.signOutAndClose(aliceSession2)
}
/**
* Check WrongBackUpVersion state
*
@ -935,6 +958,10 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun testBackupWhenAnotherBackupWasCreated() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -1005,8 +1032,11 @@ class KeysBackupTest : InstrumentedTest {
* -> It must success
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun testBackupAfterVerifyingADevice() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -1039,6 +1069,8 @@ class KeysBackupTest : InstrumentedTest {
// - Try to backup all in aliceSession2, it must fail
val keysBackup2 = aliceSession2.cryptoService().keysBackupService()
assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
val stateObserver2 = StateObserver(keysBackup2)
var isSuccessful = false
@ -1056,8 +1088,8 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(isSuccessful)
// Backup state must be NotTrusted
assertEquals(KeysBackupState.NotTrusted, keysBackup2.state)
assertFalse(keysBackup2.isEnabled)
assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.state)
assertFalse("Backup should not be enabled", keysBackup2.isEnabled)
// - Validate the old device from the new one
aliceSession2.cryptoService().setDeviceVerification(
@ -1103,6 +1135,10 @@ class KeysBackupTest : InstrumentedTest {
*/
@Test
fun deleteKeysBackupTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()

View file

@ -106,14 +106,14 @@ internal class KeysBackupTestHelper(
Assert.assertNotNull(megolmBackupCreationInfo)
Assert.assertFalse(keysBackup.isEnabled)
Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled)
// Create the version
val keysVersion = testHelper.doSync<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
Assert.assertNotNull(keysVersion.version)
Assert.assertNotNull("Key backup version should not be null", keysVersion.version)
// Backup must be enable now
Assert.assertTrue(keysBackup.isEnabled)

View file

@ -16,25 +16,35 @@
package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
/**
* A signature in a `KeysBackupVersionTrust` object.
*/
data class KeysBackupVersionTrustSignature(
/**
* The id of the device that signed the backup version.
*/
val deviceId: String?,
/**
* The device that signed the backup version.
* Can be null if the device is not known.
*/
val device: CryptoDeviceInfo?,
sealed class KeysBackupVersionTrustSignature {
/**
* Flag to indicate the signature from this device is valid.
*/
val valid: Boolean,
)
data class DeviceSignature(
/**
* The id of the device that signed the backup version.
*/
val deviceId: String?,
/**
* The device that signed the backup version.
* Can be null if the device is not known.
*/
val device: CryptoDeviceInfo?,
/**
* Flag to indicate the signature from this device is valid.
*/
val valid: Boolean) : KeysBackupVersionTrustSignature()
data class UserSignature(
val keyId: String?,
val cryptoCrossSigningKey: CryptoCrossSigningKey?,
val valid: Boolean
) : KeysBackupVersionTrustSignature()
}

View file

@ -0,0 +1,93 @@
/*
* 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.crosssigning
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import javax.inject.Inject
/**
* Holds the OlmPkSigning for cross signing.
* Can be injected without having to get the full cross signing service
*/
@SessionScope
internal class CrossSigningOlm @Inject constructor(
private val cryptoStore: IMXCryptoStore,
) {
enum class KeyType {
SELF,
USER,
MASTER
}
var olmUtility: OlmUtility = OlmUtility()
var masterPkSigning: OlmPkSigning? = null
var userPkSigning: OlmPkSigning? = null
var selfSigningPkSigning: OlmPkSigning? = null
fun release() {
olmUtility.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
}
fun signObject(type: KeyType, strToSign: String): Map<String, String> {
val myKeys = cryptoStore.getMyCrossSigningInfo()
val pubKey = when (type) {
KeyType.SELF -> myKeys?.selfSigningKey()
KeyType.USER -> myKeys?.userKey()
KeyType.MASTER -> myKeys?.masterKey()
}?.unpaddedBase64PublicKey
val pkSigning = when (type) {
KeyType.SELF -> selfSigningPkSigning
KeyType.USER -> userPkSigning
KeyType.MASTER -> masterPkSigning
}
if (pubKey == null || pkSigning == null) {
throw Throwable("Cannot sign from this account, public and/or privateKey Unknown $type|$pkSigning")
}
val signature = pkSigning.sign(strToSign)
return mapOf(
"ed25519:$pubKey" to signature
)
}
fun verifySignature(type: KeyType, signable: JsonDict, signatures: Map<String, Map<String, String>>) {
val myKeys = cryptoStore.getMyCrossSigningInfo()
?: throw NoSuchElementException("Cross Signing not configured")
val myUserID = myKeys.userId
val pubKey = when (type) {
KeyType.SELF -> myKeys.selfSigningKey()
KeyType.USER -> myKeys.userKey()
KeyType.MASTER -> myKeys.masterKey()
}?.unpaddedBase64PublicKey ?: throw NoSuchElementException("Cross Signing not configured")
val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me
?.get("ed25519:$pubKey")
if (signaturesMadeByMyKey.isNullOrBlank()) {
throw IllegalArgumentException("Not signed with my key $type")
}
// Check that Alice USK signature of Bob MSK is valid
olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
}
}

View file

@ -54,7 +54,6 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.logLimit
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -72,19 +71,13 @@ internal class DefaultCrossSigningService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val crossSigningOlm: CrossSigningOlm,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
private var masterPkSigning: OlmPkSigning? = null
private var userPkSigning: OlmPkSigning? = null
private var selfSigningPkSigning: OlmPkSigning? = null
init {
try {
olmUtility = OlmUtility()
// Try to get stored keys if they exist
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
@ -97,7 +90,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading master key success")
} else {
Timber.w("## CrossSigning - Public master key does not match the private key")
@ -110,7 +103,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning = pkSigning
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading User Signing key success")
} else {
Timber.w("## CrossSigning - Public User key does not match the private key")
@ -123,7 +116,7 @@ internal class DefaultCrossSigningService @Inject constructor(
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading Self Signing key success")
} else {
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
@ -145,8 +138,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
fun release() {
olmUtility?.releaseUtility()
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
crossSigningOlm.release()
deviceListManager.removeListener(this)
}
@ -179,9 +171,9 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
setUserKeysAsTrusted(userId, true)
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
crossSigningOlm.selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
callback.onSuccess(Unit)
}
@ -200,8 +192,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading MSK success")
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
return
@ -227,8 +219,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading SSK success")
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
return
@ -254,8 +246,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading USK success")
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
return
@ -284,8 +276,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
crossSigningOlm.masterPkSigning?.releaseSigning()
crossSigningOlm.masterPkSigning = pkSigning
masterKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -301,8 +293,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
crossSigningOlm.userPkSigning?.releaseSigning()
crossSigningOlm.userPkSigning = pkSigning
userKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -318,8 +310,8 @@ internal class DefaultCrossSigningService @Inject constructor(
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
crossSigningOlm.selfSigningPkSigning?.releaseSigning()
crossSigningOlm.selfSigningPkSigning = pkSigning
selfSignedKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -407,7 +399,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Bob MSK is valid
try {
olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
masterKeySignaturesMadeByMyUserKey,
myUserKey.unpaddedBase64PublicKey,
otherMasterKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
}
@ -461,7 +457,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity?
try {
olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
isMaterKeyTrusted = true
return@forEach
} catch (failure: Throwable) {
@ -490,7 +486,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Alice MSK is valid
try {
olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
userKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
myUserKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
}
@ -509,7 +509,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check that Alice USK signature of Alice MSK is valid
try {
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
ssKeySignaturesMadeByMyMasterKey,
myMasterKey.unpaddedBase64PublicKey,
mySSKey.canonicalSignable()
)
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
}
@ -562,7 +566,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return@launch
}
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || userPkSigning == null) {
if (userPubKey == null || crossSigningOlm.userPkSigning == null) {
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
return@launch
}
@ -571,7 +575,7 @@ internal class DefaultCrossSigningService @Inject constructor(
val newSignature = JsonCanonicalizer.getCanonicalJson(
Map::class.java,
otherMasterKeys.signalableJSONDictionary()
).let { userPkSigning?.sign(it) }
).let { crossSigningOlm.userPkSigning?.sign(it) }
if (newSignature == null) {
// race??
@ -618,13 +622,13 @@ internal class DefaultCrossSigningService @Inject constructor(
}
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
if (ssPubKey == null || selfSigningPkSigning == null) {
if (ssPubKey == null || crossSigningOlm.selfSigningPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
return@launch
}
// Sign with self signing
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
val newSignature = crossSigningOlm.selfSigningPkSigning?.sign(device.canonicalSignable())
if (newSignature == null) {
// race??
@ -697,7 +701,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
}
@ -747,7 +755,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
crossSigningOlm.olmUtility.verifyEd25519Signature(
otherSSKSignature,
otherKeys.selfSigningKey()?.unpaddedBase64PublicKey,
otherDevice.canonicalSignable()
)
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
}

View file

@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.ObjectSigner
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.crosssigning.CrossSigningOlm
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
@ -102,6 +103,7 @@ internal class DefaultKeysBackupService @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice,
private val objectSigner: ObjectSigner,
private val crossSigningOlm: CrossSigningOlm,
// Actions
private val megolmSessionDataImporter: MegolmSessionDataImporter,
// Tasks
@ -178,7 +180,6 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
}
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
SignalableMegolmBackupAuthData(
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
@ -187,7 +188,6 @@ internal class DefaultKeysBackupService @Inject constructor(
)
} else {
val publicKey = olmPkDecryption.generateKey()
SignalableMegolmBackupAuthData(
publicKey = publicKey
)
@ -195,13 +195,28 @@ internal class DefaultKeysBackupService @Inject constructor(
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 = objectSigner.signObject(canonicalJson)
signatures = signatures
)
val creationInfo = MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
authData = signedMegolmBackupAuthData,
@ -332,6 +347,10 @@ internal class DefaultKeysBackupService @Inject constructor(
override fun backupAllGroupSessions(progressListener: ProgressListener?,
callback: MatrixCallback<Unit>?) {
if (!isEnabled || backupOlmPkEncryption == null || keysBackupVersion == null) {
callback?.onFailure(Throwable("Backup not enabled"))
return
}
// Get a status right now
getBackupProgress(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
@ -420,18 +439,41 @@ internal class DefaultKeysBackupService @Inject constructor(
for ((keyId, mySignature) in mySigs) {
// XXX: is this how we're supposed to get the device id?
var deviceId: String? = null
var deviceOrCrossSigningKeyId: String? = null
val components = keyId.split(":")
if (components.size == 2) {
deviceId = components[1]
deviceOrCrossSigningKeyId = components[1]
}
if (deviceId != null) {
val device = cryptoStore.getUserDevice(userId, deviceId)
// Let's check if it's my master key
val myMSKPKey = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.unpaddedBase64PublicKey
if (deviceOrCrossSigningKeyId == myMSKPKey) {
// we have to check if we can trust
var isSignatureValid = false
try {
crossSigningOlm.verifySignature(CrossSigningOlm.KeyType.MASTER, authData.signalableJSONDictionary(), authData.signatures)
isSignatureValid = true
} catch (failure: Throwable) {
Timber.w(failure, "getKeysBackupTrust: Bad signature from my user MSK")
}
val mskTrusted = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.isVerified() == true
if (isSignatureValid && mskTrusted) {
keysBackupVersionTrustIsUsable = true
}
val signature = KeysBackupVersionTrustSignature.UserSignature(
keyId = deviceOrCrossSigningKeyId,
cryptoCrossSigningKey = cryptoStore.getMyCrossSigningInfo()?.masterKey(),
valid = isSignatureValid
)
keysBackupVersionTrustSignatures.add(signature)
} else if (deviceOrCrossSigningKeyId != null) {
val device = cryptoStore.getUserDevice(userId, deviceOrCrossSigningKeyId)
var isSignatureValid = false
if (device == null) {
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId")
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceOrCrossSigningKeyId")
} else {
val fingerprint = device.fingerprint()
if (fingerprint != null) {
@ -448,8 +490,8 @@ internal class DefaultKeysBackupService @Inject constructor(
}
}
val signature = KeysBackupVersionTrustSignature(
deviceId = deviceId,
val signature = KeysBackupVersionTrustSignature.DeviceSignature(
deviceId = deviceOrCrossSigningKeyId,
device = device,
valid = isSignatureValid,
)
@ -1220,7 +1262,6 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.v("backupKeys: Invalid configuration")
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
resetBackupAllGroupSessionsListeners()
return
}

View file

@ -128,7 +128,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
}
private fun launch4SActivity() {
SharedSecureStorageActivity.newIntent(
SharedSecureStorageActivity.newReadIntent(
context = this,
keyId = null, // default key
requestedSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME),

View file

@ -22,4 +22,8 @@ sealed class KeyBackupSettingsAction : VectorViewModelAction {
object Init : KeyBackupSettingsAction()
object GetKeyBackupTrust : KeyBackupSettingsAction()
object DeleteKeyBackup : KeyBackupSettingsAction()
object SetUpKeyBackup : KeyBackupSettingsAction()
data class StoreIn4SSuccess(val recoveryKey: String, val alias: String) : KeyBackupSettingsAction()
object StoreIn4SReset : KeyBackupSettingsAction()
object StoreIn4SFailure : KeyBackupSettingsAction()
}

View file

@ -15,6 +15,7 @@
*/
package im.vector.app.features.crypto.keysbackup.settings
import android.app.Activity
import android.content.Context
import android.content.Intent
import com.airbnb.mvrx.Fail
@ -23,9 +24,13 @@ import com.airbnb.mvrx.viewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@AndroidEntryPoint
class KeysBackupManageActivity : SimpleFragmentActivity() {
@ -41,6 +46,21 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
private val viewModel: KeysBackupSettingsViewModel by viewModel()
private val secretStartForActivityResult = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
val result = activityResult.data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
val reset = activityResult.data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false
if (result != null) {
viewModel.handle(KeyBackupSettingsAction.StoreIn4SSuccess(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS))
} else if (reset) {
// all have been reset so a new backup would have been created
viewModel.handle(KeyBackupSettingsAction.StoreIn4SReset)
}
} else {
viewModel.handle(KeyBackupSettingsAction.StoreIn4SFailure)
}
}
override fun initUiAndData() {
super.initUiAndData()
if (supportFragmentManager.fragments.isEmpty()) {
@ -69,6 +89,22 @@ class KeysBackupManageActivity : SimpleFragmentActivity() {
}
}
}
viewModel.observeViewEvents {
when (it) {
KeysBackupViewEvents.OpenLegacyCreateBackup -> {
startActivity(KeysBackupSetupActivity.intent(this, false))
}
is KeysBackupViewEvents.RequestStore4SSecret -> {
secretStartForActivityResult.launch(
SharedSecureStorageActivity.newWriteIntent(
context = this,
writeSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME to it.recoveryKey)
)
)
}
}
}
}
override fun onBackPressed() {

View file

@ -28,7 +28,6 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentKeysBackupSettingsBinding
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import javax.inject.Inject
class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSettingsRecyclerViewController: KeysBackupSettingsRecyclerViewController) :
@ -58,9 +57,7 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti
}
override fun didSelectSetupMessageRecovery() {
context?.let {
startActivity(KeysBackupSetupActivity.intent(it, false))
}
viewModel.handle(KeyBackupSettingsAction.SetUpKeyBackup)
}
override fun didSelectRestoreMessageRecovery() {

View file

@ -29,9 +29,11 @@ import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericItem
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
import java.util.UUID
import javax.inject.Inject
@ -191,69 +193,105 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(
}
}
is Success -> {
keysVersionTrust().signatures.forEach {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
} else {
if (isSignatureValid) {
if (host.session.sessionParams.deviceId == it.deviceId) {
keysVersionTrust()
.signatures
.filterIsInstance<KeysBackupVersionTrustSignature.UserSignature>()
.forEach {
val isUserVerified = it.cryptoCrossSigningKey?.trustLevel?.isVerified().orFalse()
val isSignatureValid = it.valid
val userId: String = it.cryptoCrossSigningKey?.userId ?: ""
if (userId == session.sessionParams.userId && isSignatureValid && isUserVerified) {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
.getString(R.string.keys_backup_settings_signature_from_this_user)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
if (isDeviceVerified) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
}
}
} else {
// Invalid signature
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId)
.toEpoxyCharSequence()
)
} else {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId)
.toEpoxyCharSequence()
)
}
}
}
}
} // end for each
keysVersionTrust()
.signatures
.filterIsInstance<KeysBackupVersionTrustSignature.DeviceSignature>()
.forEach {
genericItem {
id(UUID.randomUUID().toString())
title(host.stringProvider.getString(R.string.keys_backup_info_title_signature).toEpoxyCharSequence())
val isDeviceKnown = it.device != null
val isDeviceVerified = it.device?.isVerified ?: false
val isSignatureValid = it.valid
val deviceId: String = it.deviceId ?: ""
if (!isDeviceKnown) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
} else {
if (isSignatureValid) {
if (host.session.sessionParams.deviceId == it.deviceId) {
description(
host.stringProvider
.getString(R.string.keys_backup_settings_valid_signature_from_this_device)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
if (isDeviceVerified) {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_valid_signature_from_verified_device,
deviceId
)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_verified)
} else {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_valid_signature_from_unverified_device,
deviceId
)
.toEpoxyCharSequence()
)
endIconResourceId(R.drawable.e2e_warning)
}
}
} else {
// Invalid signature
endIconResourceId(R.drawable.e2e_warning)
if (isDeviceVerified) {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_invalid_signature_from_verified_device,
deviceId
)
.toEpoxyCharSequence()
)
} else {
description(
host.stringProvider
.getString(
R.string.keys_backup_settings_invalid_signature_from_unverified_device,
deviceId
)
.toEpoxyCharSequence()
)
}
}
}
}
} // end for each
}
is Fail -> {
errorWithRetryItem {

View file

@ -25,8 +25,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.session.Session
@ -34,10 +34,16 @@ 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.KeysBackupStateListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
import timber.log.Timber
class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState,
session: Session
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction, EmptyViewEvents>(initialState),
private val session: Session
) : VectorViewModel<KeysBackupSettingViewState, KeyBackupSettingsAction, KeysBackupViewEvents>(initialState),
KeysBackupStateListener {
@AssistedFactory
@ -49,6 +55,8 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
private val keysBackupService: KeysBackupService = session.cryptoService().keysBackupService()
var pendingBackupCreationInfo: MegolmBackupCreationInfo? = null
init {
setState {
this.copy(
@ -62,9 +70,18 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
override fun handle(action: KeyBackupSettingsAction) {
when (action) {
KeyBackupSettingsAction.Init -> init()
KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust()
KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup()
KeyBackupSettingsAction.Init -> init()
KeyBackupSettingsAction.GetKeyBackupTrust -> getKeysBackupTrust()
KeyBackupSettingsAction.DeleteKeyBackup -> deleteCurrentBackup()
KeyBackupSettingsAction.SetUpKeyBackup -> viewModelScope.launch {
setUpKeyBackup()
}
KeyBackupSettingsAction.StoreIn4SReset,
KeyBackupSettingsAction.StoreIn4SFailure -> {
pendingBackupCreationInfo = null
// nothing to do just stay on fragment
}
is KeyBackupSettingsAction.StoreIn4SSuccess -> viewModelScope.launch { completeBackupCreation() }
}
}
@ -120,6 +137,41 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
getKeysBackupTrust()
}
suspend fun setUpKeyBackup() {
// We need to check if 4S is enabled first.
// If it is we need to use it, generate a random key
// for the backup and store it in the 4S
if (session.sharedSecretStorageService().isRecoverySetup()) {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
pendingBackupCreationInfo = creationInfo
val recoveryKey = extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()
_viewEvents.post(KeysBackupViewEvents.RequestStore4SSecret(recoveryKey!!))
} else {
// No 4S so we can open legacy flow
_viewEvents.post(KeysBackupViewEvents.OpenLegacyCreateBackup)
}
}
suspend fun completeBackupCreation() {
val info = pendingBackupCreationInfo ?: return
try {
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(info, it)
}
// Save it for gossiping
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(info.recoveryKey, version = version.version)
} catch (failure: Throwable) {
// XXX mm... failed we should remove what we put in 4S, as it was not created?
// for now just stay on the screen, user can retry, there is no api to delete account data
} finally {
pendingBackupCreationInfo = null
}
}
private fun deleteCurrentBackup() {
val keysBackupService = keysBackupService

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.crypto.keysbackup.settings
import im.vector.app.core.platform.VectorViewEvents
sealed class KeysBackupViewEvents : VectorViewEvents {
object OpenLegacyCreateBackup : KeysBackupViewEvents()
data class RequestStore4SSecret(val recoveryKey: String) : KeysBackupViewEvents()
}

View file

@ -48,8 +48,9 @@ class SharedSecureStorageActivity :
@Parcelize
data class Args(
val keyId: String?,
val requestedSecrets: List<String>,
val resultKeyStoreAlias: String
val requestedSecrets: List<String> = emptyList(),
val resultKeyStoreAlias: String,
val writeSecrets: List<Pair<String, String>> = emptyList(),
) : Parcelable
private val viewModel: SharedSecureStorageViewModel by viewModel()
@ -148,18 +149,36 @@ class SharedSecureStorageActivity :
const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(context: Context,
keyId: String? = null,
requestedSecrets: List<String>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
fun newReadIntent(context: Context,
keyId: String? = null,
requestedSecrets: List<String>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
require(requestedSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also {
it.putExtra(
Mavericks.KEY_ARG, Args(
keyId,
requestedSecrets,
resultKeyStoreAlias
Mavericks.KEY_ARG,
Args(
keyId = keyId,
requestedSecrets = requestedSecrets,
resultKeyStoreAlias = resultKeyStoreAlias
)
)
}
}
fun newWriteIntent(context: Context,
keyId: String? = null,
writeSecrets: List<Pair<String, String>>,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
require(writeSecrets.isNotEmpty())
return Intent(context, SharedSecureStorageActivity::class.java).also {
it.putExtra(
Mavericks.KEY_ARG,
Args(
keyId = keyId,
writeSecrets = writeSecrets,
resultKeyStoreAlias = resultKeyStoreAlias
)
)
}
}

View file

@ -39,13 +39,20 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfo
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
import java.io.ByteArrayOutputStream
sealed class RequestType {
data class ReadSecrets(val secretsName: List<String>) : RequestType()
data class WriteSecrets(val secretsNameValue: List<Pair<String, String>>) : RequestType()
}
data class SharedSecureStorageViewState(
val ready: Boolean = false,
val hasPassphrase: Boolean = true,
@ -55,13 +62,17 @@ data class SharedSecureStorageViewState(
val showResetAllAction: Boolean = false,
val userId: String = "",
val keyId: String?,
val requestedSecrets: List<String>,
val requestType: RequestType,
val resultKeyStoreAlias: String
) : MavericksState {
constructor(args: SharedSecureStorageActivity.Args) : this(
keyId = args.keyId,
requestedSecrets = args.requestedSecrets,
requestType = if (args.writeSecrets.isNotEmpty()) {
RequestType.WriteSecrets(args.writeSecrets)
} else {
RequestType.ReadSecrets(args.requestedSecrets)
},
resultKeyStoreAlias = args.resultKeyStoreAlias
)
@ -87,14 +98,17 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
setState {
copy(userId = session.myUserId)
}
val integrityResult = session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestedSecrets, initialState.keyId)
if (integrityResult !is IntegrityResult.Success) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
if (initialState.requestType is RequestType.ReadSecrets) {
val integrityResult =
session.sharedSecretStorageService().checkShouldBeAbleToAccessSecrets(initialState.requestType.secretsName, initialState.keyId)
if (integrityResult !is IntegrityResult.Success) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
}
}
val keyResult = initialState.keyId?.let { session.sharedSecretStorageService().getKey(it) }
?: session.sharedSecretStorageService().getDefaultKey()
@ -226,20 +240,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) }
}
withContext(Dispatchers.IO) {
initialState.requestedSecrets.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
}
performRequest(keyInfo, keySpec, decryptedSecretMap)
}
}.fold({
setState { copy(checkingSSSSAction = Success(Unit)) }
@ -258,6 +260,37 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
}
}
private suspend fun performRequest(keyInfo: KeyInfo, keySpec: RawBytesKeySpec, decryptedSecretMap: HashMap<String, String>) {
when (val requestType = initialState.requestType) {
is RequestType.ReadSecrets -> {
requestType.secretsName.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
}
}
is RequestType.WriteSecrets -> {
requestType.secretsNameValue.forEach {
val (name, value) = it
session.sharedSecretStorageService().storeSecret(
name = name,
secretBase64 = value,
keys = listOf(SharedSecretStorageService.KeyRef(keyInfo.id, keySpec))
)
decryptedSecretMap[name] = value
}
}
}
}
private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val decryptedSecretMap = HashMap<String, String>()
@ -302,17 +335,8 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
)
withContext(Dispatchers.IO) {
initialState.requestedSecrets.forEach {
if (session.accountDataService().getUserAccountDataEvent(it) != null) {
val res = session.sharedSecretStorageService().getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec
)
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
withContext(Dispatchers.IO) {
performRequest(keyInfo, keySpec, decryptedSecretMap)
}
}
}.fold({

View file

@ -95,14 +95,12 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetV
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.AccessSecretStore -> {
secretStartForActivityResult.launch(
SharedSecureStorageActivity.newIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
)
)
secretStartForActivityResult.launch(SharedSecureStorageActivity.newReadIntent(
requireContext(),
null, // use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME),
SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
))
}
is VerificationBottomSheetViewEvents.ModalError -> {
MaterialAlertDialogBuilder(requireContext())

View file

@ -1513,6 +1513,7 @@
<string name="keys_backup_settings_status_not_setup">Your keys are not being backed up from this session.</string>
<string name="keys_backup_settings_signature_from_unknown_device">Backup has a signature from unknown session with ID %s.</string>
<string name="keys_backup_settings_signature_from_this_user">Backup has a valid signature from this user.</string>
<string name="keys_backup_settings_valid_signature_from_this_device">Backup has a valid signature from this session.</string>
<string name="keys_backup_settings_valid_signature_from_verified_device">Backup has a valid signature from verified session %s.</string>
<string name="keys_backup_settings_valid_signature_from_unverified_device">Backup has a valid signature from unverified session %s</string>