mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Merge pull request #5939 from vector-im/feature/bca/fix_5906
Desynchronized 4S | Megolm backup causing Unusable backup
This commit is contained in:
commit
d40f8b0c56
19 changed files with 699 additions and 280 deletions
1
changelog.d/5906.bugfix
Normal file
1
changelog.d/5906.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Desynchronized 4S | Megolm backup causing Unusable backup
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue