diff --git a/.gitignore b/.gitignore index 421ac09561..9f442cce2b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ ktlint .idea/copyright/New_vector.xml .idea/copyright/profiles_settings.xml + +.idea/copyright/New_Vector_Ltd.xml diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 406e274258..046ff55a3f 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -18,6 +18,7 @@ package im.vector.matrix.rx import androidx.paging.PagedList import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.pushers.Pusher @@ -29,6 +30,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import io.reactivex.Observable import io.reactivex.Single @@ -98,6 +100,15 @@ class RxSession(private val session: Session) { fun getProfileInfo(userId: String): Single = singleBuilder { session.getProfile(userId, it) } + + fun liveUserCryptoDevices(userId: String): Observable> { + return session.getLiveCryptoDeviceInfo(userId).asObservable() + } + + fun liveCrossSigningInfo(userId: String): Observable> { + return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable() + .startWith(session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional()) + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt index 99fe7d29b4..56b358c69f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/InstrumentedTest.kt @@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider import java.io.File interface InstrumentedTest { + fun context(): Context { return ApplicationProvider.getApplicationContext() } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index b16c865765..97ed8ca6c5 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.common import android.content.Context import android.net.Uri +import androidx.lifecycle.Observer import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixConfiguration @@ -31,6 +32,11 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings +import im.vector.matrix.android.api.session.sync.SyncState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.junit.Assert.* import java.util.* import java.util.concurrent.CountDownLatch @@ -73,23 +79,25 @@ class CommonTestHelper(context: Context) { * @param session the session to sync */ fun syncSession(session: Session) { - // val lock = CountDownLatch(1) - - // val observer = androidx.lifecycle.Observer { syncState -> - // if (syncState is SyncState.Idle) { - // lock.countDown() - // } - // } - - // TODO observe? - // while (session.syncState().value !is SyncState.Idle) { - // sleep(100) - // } + val lock = CountDownLatch(1) session.open() session.startSync(true) - // await(lock) - // session.syncState().removeObserver(observer) + + val syncLiveData = runBlocking(Dispatchers.Main) { + session.getSyncStateLive() + } + val syncObserver = object : Observer { + override fun onChanged(t: SyncState?) { + if (session.hasAlreadySynced()) { + lock.countDown() + syncLiveData.removeObserver(this) + } + } + } + GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) } + + await(lock) } /** diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index df45249265..25cfff5c16 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -17,11 +17,15 @@ package im.vector.matrix.android.common import android.os.SystemClock +import androidx.lifecycle.Observer import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineSettings @@ -29,6 +33,10 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.junit.Assert.* import java.util.* import java.util.concurrent.CountDownLatch @@ -78,26 +86,31 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId - val room = aliceSession.getRoom(aliceRoomId)!! + val aliceRoom = aliceSession.getRoom(aliceRoomId)!! val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) val lock1 = CountDownLatch(2) -// val bobEventListener = object : MXEventListener() { -// override fun onNewRoom(roomId: String) { -// if (TextUtils.equals(roomId, aliceRoomId)) { -// if (!statuses.containsKey("onNewRoom")) { -// statuses["onNewRoom"] = "onNewRoom" -// lock1.countDown() -// } -// } -// } -// } -// -// bobSession.dataHandler.addListener(bobEventListener) + val bobRoomSummariesLive = runBlocking(Dispatchers.Main) { + bobSession.getRoomSummariesLive(roomSummaryQueryParams { }) + } - room.invite(bobSession.myUserId, callback = object : TestMatrixCallback(lock1) { + val newRoomObserver = object : Observer> { + override fun onChanged(t: List?) { + if (t?.isNotEmpty() == true) { + statuses["onNewRoom"] = "onNewRoom" + lock1.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } + + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(newRoomObserver) + } + + aliceRoom.invite(bobSession.myUserId, callback = object : TestMatrixCallback(lock1) { override fun onSuccess(data: Unit) { statuses["invite"] = "invite" super.onSuccess(data) @@ -108,25 +121,25 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) -// bobSession.dataHandler.removeListener(bobEventListener) - val lock2 = CountDownLatch(2) - bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2)) + val roomJoinedObserver = object : Observer> { + override fun onChanged(t: List?) { + if (bobSession.getRoom(aliceRoomId) + ?.getRoomMember(aliceSession.myUserId) + ?.membership == Membership.JOIN) { + statuses["AliceJoin"] = "AliceJoin" + lock2.countDown() + bobRoomSummariesLive.removeObserver(this) + } + } + } -// room.addEventListener(object : MXEventListener() { -// override fun onLiveEvent(event: Event, roomState: RoomState) { -// if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) { -// val contentToConsider = event.contentAsJsonObject -// val member = JsonUtils.toRoomMember(contentToConsider) -// -// if (TextUtils.equals(member.membership, RoomMember.MEMBERSHIP_JOIN)) { -// statuses["AliceJoin"] = "AliceJoin" -// lock2.countDown() -// } -// } -// } -// }) + GlobalScope.launch(Dispatchers.Main) { + bobRoomSummariesLive.observeForever(roomJoinedObserver) + } + + bobSession.joinRoom(aliceRoomId, callback = TestMatrixCallback(lock2)) mTestHelper.await(lock2) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt new file mode 100644 index 0000000000..605fcd5f76 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -0,0 +1,212 @@ +package im.vector.matrix.android.internal.crypto.crosssigning + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.common.TestMatrixCallback +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class XSigningTest : InstrumentedTest { + + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun test_InitializeAndStoreKeys() { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + + val aliceLatch = CountDownLatch(1) + aliceSession.getCrossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), TestMatrixCallback(aliceLatch)) + + mTestHelper.await(aliceLatch) + + val myCrossSigningKeys = aliceSession.getCrossSigningService().getMyCrossSigningKeys() + val masterPubKey = myCrossSigningKeys?.masterKey() + assertNotNull("Master key should be stored", masterPubKey?.unpaddedBase64PublicKey) + val selfSigningKey = myCrossSigningKeys?.selfSigningKey() + assertNotNull("SelfSigned key should be stored", selfSigningKey?.unpaddedBase64PublicKey) + val userKey = myCrossSigningKeys?.userKey() + assertNotNull("User key should be stored", userKey?.unpaddedBase64PublicKey) + + assertTrue("Signing Keys should be trusted", myCrossSigningKeys?.isTrusted() == true) + + assertTrue("Signing Keys should be trusted", aliceSession.getCrossSigningService().checkUserTrust(aliceSession.myUserId).isVerified()) + + mTestHelper.signout(aliceSession) + } + + @Test + fun test_CrossSigningCheckBobSeesTheKeys() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + val aliceLatch = CountDownLatch(1) + val bobLatch = CountDownLatch(1) + + aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(aliceLatch)) + bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(bobLatch)) + + mTestHelper.await(aliceLatch) + mTestHelper.await(bobLatch) + + // Check that alice can see bob keys + val downloadLatch = CountDownLatch(1) + aliceSession.downloadKeys(listOf(bobSession.myUserId), true, TestMatrixCallback(downloadLatch)) + mTestHelper.await(downloadLatch) + + val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobSession.myUserId) + assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV?.masterKey()) + assertNull("Alice should not see bob User key", bobKeysFromAlicePOV?.userKey()) + assertNotNull("Alice can see bob SelfSigned key", bobKeysFromAlicePOV?.selfSigningKey()) + + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.masterKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.masterKey()?.unpaddedBase64PublicKey) + assertEquals("Bob keys from alice pov should match", bobKeysFromAlicePOV?.selfSigningKey()?.unpaddedBase64PublicKey, bobSession.getCrossSigningService().getMyCrossSigningKeys()?.selfSigningKey()?.unpaddedBase64PublicKey) + + assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) + + mTestHelper.signout(aliceSession) + mTestHelper.signout(bobSession) + } + + @Test + fun test_CrossSigningTestAliceTrustBobNewDevice() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession + + val aliceAuthParams = UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ) + val bobAuthParams = UserPasswordAuth( + user = bobSession!!.myUserId, + password = TestConstants.PASSWORD + ) + + val aliceLatch = CountDownLatch(1) + val bobLatch = CountDownLatch(1) + + aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(aliceLatch)) + bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(bobLatch)) + + mTestHelper.await(aliceLatch) + mTestHelper.await(bobLatch) + + // Check that alice can see bob keys + val downloadLatch = CountDownLatch(1) + val bobUserId = bobSession.myUserId + aliceSession.downloadKeys(listOf(bobUserId), true, TestMatrixCallback(downloadLatch)) + mTestHelper.await(downloadLatch) + + val bobKeysFromAlicePOV = aliceSession.getCrossSigningService().getUserCrossSigningKeys(bobUserId) + assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) + + val trustLatch = CountDownLatch(1) + aliceSession.getCrossSigningService().trustUser(bobUserId, object : MatrixCallback { + override fun onSuccess(data: Unit) { + trustLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + fail("Failed to trust bob") + } + }) + mTestHelper.await(trustLatch) + + // Now bobs logs in on a new device and verifies it + // We will want to test that in alice POV, this new device would be trusted by cross signing + + val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) + val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId + + // Check that bob first session sees the new login + val bobKeysLatch = CountDownLatch(1) + bobSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback> { + override fun onFailure(failure: Throwable) { + fail("Failed to get device") + } + + override fun onSuccess(data: MXUsersDevicesMap) { + if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId!!) == false) { + fail("Bob should see the new device") + } + bobKeysLatch.countDown() + } + }) + mTestHelper.await(bobKeysLatch) + + val bobSecondDevicePOVFirstDevice = bobSession.getDeviceInfo(bobUserId, bobSecondDeviceId) + assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) + + // Manually mark it as trusted from first session + val bobSignLatch = CountDownLatch(1) + bobSession.getCrossSigningService().signDevice(bobSecondDeviceId!!, object : MatrixCallback { + override fun onSuccess(data: Unit) { + bobSignLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + fail("Failed to trust bob ${failure.localizedMessage}") + } + }) + mTestHelper.await(bobSignLatch) + + // Now alice should cross trust bob's second device + val aliceKeysLatch = CountDownLatch(1) + aliceSession.downloadKeys(listOf(bobUserId), true, object : MatrixCallback> { + override fun onFailure(failure: Throwable) { + fail("Failed to get device") + } + + override fun onSuccess(data: MXUsersDevicesMap) { + // check that the device is seen + if (data.getUserDeviceIds(bobUserId)?.contains(bobSecondDeviceId) == false) { + fail("Alice should see the new device") + } + aliceKeysLatch.countDown() + } + }) + mTestHelper.await(aliceKeysLatch) + + val result = aliceSession.getCrossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null) + assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified()) + + mTestHelper.signout(aliceSession) + mTestHelper.signout(bobSession) + mTestHelper.signout(bobSession2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index 15deebdab1..03dba96d40 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -25,23 +25,36 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener -import im.vector.matrix.android.common.* +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestData +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.common.TestMatrixCallback +import im.vector.matrix.android.common.assertDictEquals +import im.vector.matrix.android.common.assertListEquals import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters -import java.util.* +import java.util.ArrayList +import java.util.Collections import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @@ -298,7 +311,10 @@ class KeysBackupTest : InstrumentedTest { val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg - val sessionData = keysBackup.decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) + val sessionData = keysBackup + .decryptKeyBackupData(keyBackupData, + session.olmInboundGroupSession!!.sessionIdentifier(), + cryptoTestData.roomId, decryption!!) assertNotNull(sessionData) // - Compare the decrypted megolm key with the original one assertKeysEquals(session.exportKeys(), sessionData) @@ -1161,7 +1177,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup2.isEnabled) // - Validate the old device from the new one - aliceSession2.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, oldDeviceId, aliceSession2.myUserId) + aliceSession2.setDeviceVerification(DeviceTrustLevel(false, true), aliceSession2.myUserId, oldDeviceId) // -> Backup should automatically enable on the new device val latch4 = CountDownLatch(1) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index c05523f009..06310e7566 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -24,11 +24,12 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestHelper -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.model.rest.toValue import org.junit.Assert.* import org.junit.FixMethodOrder import org.junit.Test @@ -50,53 +51,53 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceSasMgr = aliceSession.getSasVerificationService() - val bobSasMgr = bobSession!!.getSasVerificationService() + val aliceVerificationService = aliceSession.getVerificationService() + val bobVerificationService = bobSession!!.getVerificationService() val bobTxCreatedLatch = CountDownLatch(1) - val bobListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val bobListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { + override fun transactionUpdated(tx: VerificationTransaction) { bobTxCreatedLatch.countDown() } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - bobSasMgr.addListener(bobListener) + bobVerificationService.addListener(bobListener) - val txID = aliceSasMgr.beginKeyVerificationSAS(bobSession.myUserId, bobSession.getMyDevice().deviceId) + val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, bobSession.getMyDevice().deviceId) assertNotNull("Alice should have a started transaction", txID) - val aliceKeyTx = aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID!!) + val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!) assertNotNull("Alice should have a started transaction", aliceKeyTx) mTestHelper.await(bobTxCreatedLatch) - bobSasMgr.removeListener(bobListener) + bobVerificationService.removeListener(bobListener) - val bobKeyTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID) + val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID) assertNotNull("Bob should have started verif transaction", bobKeyTx) - assertTrue(bobKeyTx is SASVerificationTransaction) + assertTrue(bobKeyTx is SASDefaultVerificationTransaction) assertNotNull("Bob should have starting a SAS transaction", bobKeyTx) - assertTrue(aliceKeyTx is SASVerificationTransaction) + assertTrue(aliceKeyTx is SASDefaultVerificationTransaction) assertEquals("Alice and Bob have same transaction id", aliceKeyTx!!.transactionId, bobKeyTx!!.transactionId) - val aliceSasTx = aliceKeyTx as SASVerificationTransaction? - val bobSasTx = bobKeyTx as SASVerificationTransaction? + val aliceSasTx = aliceKeyTx as SASDefaultVerificationTransaction? + val bobSasTx = bobKeyTx as SASDefaultVerificationTransaction? - assertEquals("Alice state should be started", SasVerificationTxState.Started, aliceSasTx!!.state) - assertEquals("Bob state should be started by alice", SasVerificationTxState.OnStarted, bobSasTx!!.state) + assertEquals("Alice state should be started", VerificationTxState.Started, aliceSasTx!!.state) + assertEquals("Bob state should be started by alice", VerificationTxState.OnStarted, bobSasTx!!.state) // Let's cancel from alice side val cancelLatch = CountDownLatch(1) - val bobListener2 = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val bobListener2 = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { + override fun transactionUpdated(tx: VerificationTransaction) { if (tx.transactionId == txID) { - if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) { + if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnCancelled) { cancelLatch.countDown() } } @@ -104,23 +105,23 @@ class SASTest : InstrumentedTest { override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - bobSasMgr.addListener(bobListener2) + bobVerificationService.addListener(bobListener2) aliceSasTx.cancel(CancelCode.User) mTestHelper.await(cancelLatch) assertEquals("Should be cancelled on alice side", - SasVerificationTxState.Cancelled, aliceSasTx.state) + VerificationTxState.Cancelled, aliceSasTx.state) assertEquals("Should be cancelled on bob side", - SasVerificationTxState.OnCancelled, bobSasTx.state) + VerificationTxState.OnCancelled, bobSasTx.state) assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceSasTx.cancelledReason) assertEquals("Should be User cancelled on bob side", CancelCode.User, aliceSasTx.cancelledReason) - assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)) - assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID)) + assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) + assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) cryptoTestData.close() } @@ -135,8 +136,23 @@ class SASTest : InstrumentedTest { val tid = "00000000" // Bob should receive a cancel - var canceledToDeviceEvent: Event? = null + var cancelReason: String? = null val cancelLatch = CountDownLatch(1) + + val bobListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} + + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.transactionId == tid && tx.cancelledReason != null) { + cancelReason = tx.cancelledReason?.humanReadable + cancelLatch.countDown() + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) {} + } + bobSession.getVerificationService().addListener(bobListener) + // TODO bobSession!!.dataHandler.addListener(object : MXEventListener() { // TODO override fun onToDeviceEvent(event: Event?) { // TODO if (event!!.getType() == CryptoEvent.EVENT_TYPE_KEY_VERIFICATION_CANCEL) { @@ -152,25 +168,24 @@ class SASTest : InstrumentedTest { val aliceUserID = aliceSession.myUserId val aliceDevice = aliceSession.getMyDevice().deviceId - val aliceListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val aliceListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - (tx as IncomingSASVerificationTransaction).performAccept() + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + (tx as IncomingSasVerificationTransaction).performAccept() } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - aliceSession.getSasVerificationService().addListener(aliceListener) + aliceSession.getVerificationService().addListener(aliceListener) fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols) mTestHelper.await(cancelLatch) - val cancelReq = canceledToDeviceEvent!!.content.toModel()!! - assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code) + assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReason) cryptoTestData.close() } @@ -253,18 +268,19 @@ class SASTest : InstrumentedTest { aliceUserID: String?, aliceDevice: String?, tid: String, - protocols: List = SASVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, - hashes: List = SASVerificationTransaction.KNOWN_HASHES, - mac: List = SASVerificationTransaction.KNOWN_MACS, - codes: List = SASVerificationTransaction.KNOWN_SHORT_CODES) { - val startMessage = KeyVerificationStart() - startMessage.fromDevice = bobSession.getMyDevice().deviceId - startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS - startMessage.transactionID = tid - startMessage.keyAgreementProtocols = protocols - startMessage.hashes = hashes - startMessage.messageAuthenticationCodes = mac - startMessage.shortAuthenticationStrings = codes + protocols: List = SASDefaultVerificationTransaction.KNOWN_AGREEMENT_PROTOCOLS, + hashes: List = SASDefaultVerificationTransaction.KNOWN_HASHES, + mac: List = SASDefaultVerificationTransaction.KNOWN_MACS, + codes: List = SASDefaultVerificationTransaction.KNOWN_SHORT_CODES) { + val startMessage = KeyVerificationStart( + fromDevice = bobSession.getMyDevice().deviceId, + method = VerificationMethod.SAS.toValue(), + transactionID = tid, + keyAgreementProtocols = protocols, + hashes = hashes, + messageAuthenticationCodes = mac, + shortAuthenticationStrings = codes + ) val contentMap = MXUsersDevicesMap() contentMap.setObject(aliceUserID, aliceDevice, startMessage) @@ -287,31 +303,31 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceSasMgr = aliceSession.getSasVerificationService() + val aliceVerificationService = aliceSession.getVerificationService() val aliceCreatedLatch = CountDownLatch(2) val aliceCancelledLatch = CountDownLatch(2) - val createdTx = ArrayList() - val aliceListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) { - createdTx.add(tx as SASVerificationTransaction) + val createdTx = ArrayList() + val aliceListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) { + createdTx.add(tx as SASDefaultVerificationTransaction) aliceCreatedLatch.countDown() } - override fun transactionUpdated(tx: SasVerificationTransaction) { - if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnCancelled) { + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnCancelled) { aliceCancelledLatch.countDown() } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - aliceSasMgr.addListener(aliceListener) + aliceVerificationService.addListener(aliceListener) val bobUserId = bobSession!!.myUserId val bobDeviceId = bobSession.getMyDevice().deviceId - aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) - aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId) mTestHelper.await(aliceCreatedLatch) mTestHelper.await(aliceCancelledLatch) @@ -329,46 +345,46 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceSasMgr = aliceSession.getSasVerificationService() - val bobSasMgr = bobSession!!.getSasVerificationService() + val aliceVerificationService = aliceSession.getVerificationService() + val bobVerificationService = bobSession!!.getVerificationService() var accepted: KeyVerificationAccept? = null var startReq: KeyVerificationStart? = null val aliceAcceptedLatch = CountDownLatch(1) - val aliceListener = object : SasVerificationService.SasVerificationListener { + val aliceListener = object : VerificationService.VerificationListener { override fun markedAsManuallyVerified(userId: String, deviceId: String) {} - override fun transactionCreated(tx: SasVerificationTransaction) {} + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - if ((tx as SASVerificationTransaction).state === SasVerificationTxState.OnAccepted) { - val at = tx as SASVerificationTransaction - accepted = at.accepted - startReq = at.startReq + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { + val at = tx as SASDefaultVerificationTransaction + accepted = at.accepted as? KeyVerificationAccept + startReq = at.startReq as? KeyVerificationStart aliceAcceptedLatch.countDown() } } } - aliceSasMgr.addListener(aliceListener) + aliceVerificationService.addListener(aliceListener) - val bobListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val bobListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - if ((tx as IncomingSASVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { - val at = tx as IncomingSASVerificationTransaction + override fun transactionUpdated(tx: VerificationTransaction) { + if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { + val at = tx as IncomingSasVerificationTransaction at.performAccept() } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - bobSasMgr.addListener(bobListener) + bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId val bobDeviceId = bobSession.getMyDevice().deviceId - aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId) mTestHelper.await(aliceAcceptedLatch) assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) @@ -393,38 +409,38 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceSasMgr = aliceSession.getSasVerificationService() - val bobSasMgr = bobSession!!.getSasVerificationService() + val aliceVerificationService = aliceSession.getVerificationService() + val bobVerificationService = bobSession!!.getVerificationService() val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val aliceListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - val uxState = (tx as OutgoingSASVerificationRequest).uxState + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState when (uxState) { - OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { aliceSASLatch.countDown() } - else -> Unit + else -> Unit } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - aliceSasMgr.addListener(aliceListener) + aliceVerificationService.addListener(aliceListener) val bobSASLatch = CountDownLatch(1) - val bobListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val bobListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - val uxState = (tx as IncomingSASVerificationTransaction).uxState + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState when (uxState) { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { tx.performAccept() } - else -> Unit + else -> Unit } if (uxState === IncomingSasVerificationTransaction.UxState.SHOW_SAS) { bobSASLatch.countDown() @@ -433,16 +449,16 @@ class SASTest : InstrumentedTest { override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - bobSasMgr.addListener(bobListener) + bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId val bobDeviceId = bobSession.getMyDevice().deviceId - val verificationSAS = aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId) mTestHelper.await(aliceSASLatch) mTestHelper.await(bobSASLatch) - val aliceTx = aliceSasMgr.getExistingTransaction(bobUserId, verificationSAS!!) as SASVerificationTransaction - val bobTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASVerificationTransaction + val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction + val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL), bobTx.getShortCodeRepresentation(SasMode.DECIMAL)) @@ -457,36 +473,36 @@ class SASTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession - val aliceSasMgr = aliceSession.getSasVerificationService() - val bobSasMgr = bobSession!!.getSasVerificationService() + val aliceVerificationService = aliceSession.getVerificationService() + val bobVerificationService = bobSession!!.getVerificationService() val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val aliceListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - val uxState = (tx as OutgoingSASVerificationRequest).uxState + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as OutgoingSasVerificationTransaction).uxState when (uxState) { - OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { tx.userHasVerifiedShortCode() } - OutgoingSasVerificationRequest.UxState.VERIFIED -> { + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { aliceSASLatch.countDown() } - else -> Unit + else -> Unit } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - aliceSasMgr.addListener(aliceListener) + aliceVerificationService.addListener(aliceListener) val bobSASLatch = CountDownLatch(1) - val bobListener = object : SasVerificationService.SasVerificationListener { - override fun transactionCreated(tx: SasVerificationTransaction) {} + val bobListener = object : VerificationService.VerificationListener { + override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: SasVerificationTransaction) { - val uxState = (tx as IncomingSASVerificationTransaction).uxState + override fun transactionUpdated(tx: VerificationTransaction) { + val uxState = (tx as IncomingSasVerificationTransaction).uxState when (uxState) { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { tx.performAccept() @@ -497,23 +513,23 @@ class SASTest : InstrumentedTest { IncomingSasVerificationTransaction.UxState.VERIFIED -> { bobSASLatch.countDown() } - else -> Unit + else -> Unit } } override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } - bobSasMgr.addListener(bobListener) + bobVerificationService.addListener(bobListener) val bobUserId = bobSession.myUserId val bobDeviceId = bobSession.getMyDevice().deviceId - aliceSasMgr.beginKeyVerificationSAS(bobUserId, bobDeviceId) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId) mTestHelper.await(aliceSASLatch) mTestHelper.await(bobSASLatch) // Assert that devices are verified - val bobDeviceInfoFromAlicePOV: MXDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId) - val aliceDeviceInfoFromBobPOV: MXDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId) + val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.getDeviceInfo(bobUserId, bobDeviceId) + val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? = bobSession.getDeviceInfo(aliceSession.myUserId, aliceSession.getMyDevice().deviceId) // latch wait a bit again Thread.sleep(1000) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt new file mode 100644 index 0000000000..7a07c16d14 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecretTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldNotBeEqualTo +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SharedSecretTest : InstrumentedTest { + + @Test + fun testSharedSecretLengthCase() { + repeat(100) { + generateSharedSecret().length shouldBe 43 + } + } + + @Test + fun testSharedDiffCase() { + val sharedSecret1 = generateSharedSecret() + val sharedSecret2 = generateSharedSecret() + + sharedSecret1 shouldNotBeEqualTo sharedSecret2 + } +} diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml index 7191d9c8d5..e8762b21f2 100644 --- a/matrix-sdk-android/src/main/AndroidManifest.xml +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -6,9 +6,10 @@ - + - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt index bada3f86a1..23e8c70386 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -17,14 +17,14 @@ package im.vector.matrix.android.api.extensions import im.vector.matrix.android.api.comparators.DatedObjectComparators -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo /* ========================================================================================== * MXDeviceInfo * ========================================================================================== */ -fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() +fun CryptoDeviceInfo.getFingerprintHumanReadable() = fingerprint() ?.chunked(4) ?.joinToString(separator = " ") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt index 1af77869ee..03c5149e6b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.permalinks import im.vector.matrix.android.api.session.events.model.Event /** - * Useful methods to create Matrix permalink. + * Useful methods to create Matrix permalink (matrix.to links). */ object PermalinkFactory { @@ -84,7 +84,17 @@ object PermalinkFactory { * @param id the id to escape * @return the escaped id */ - private fun escape(id: String): String { + internal fun escape(id: String): String { return id.replace("/", "%2F") } + + /** + * Unescape '/' in id + * + * @param id the id to escape + * @return the escaped id + */ + internal fun unescape(id: String): String { + return id.replace("%2F", "/") + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 986cbb698b..594ad39063 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -17,15 +17,19 @@ package im.vector.matrix.android.api.session.crypto import android.content.Context +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.VerificationService import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult @@ -46,7 +50,9 @@ interface CryptoService { fun isCryptoEnabled(): Boolean - fun getSasVerificationService(): SasVerificationService + fun getVerificationService(): VerificationService + + fun getCrossSigningService(): CrossSigningService fun getKeysBackupService(): KeysBackupService @@ -54,15 +60,15 @@ interface CryptoService { fun setWarnOnUnknownDevices(warn: Boolean) - fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String) + fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) - fun getUserDevices(userId: String): MutableList + fun getUserDevices(userId: String): MutableList fun setDevicesKnown(devices: List, callback: MatrixCallback?) - fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? + fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? - fun getMyDevice(): MXDeviceInfo + fun getMyDevice(): CryptoDeviceInfo fun getGlobalBlacklistUnverifiedDevices(): Boolean @@ -78,7 +84,7 @@ interface CryptoService { fun setRoomBlacklistUnverifiedDevices(roomId: String) - fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? + fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? fun reRequestRoomKeyForEvent(event: Event) @@ -110,7 +116,11 @@ interface CryptoService { fun shouldEncryptForInvitedMembers(roomId: String): Boolean - fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) + fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) + + fun getCryptoDeviceInfo(userId: String): List + + fun getLiveCryptoDeviceInfo(userId: String): LiveData> fun addNewSessionListener(newSessionListener: NewSessionListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt index fe41b6c074..be817c70cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.crypto -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import org.matrix.olm.OlmException @@ -36,7 +36,7 @@ sealed class MXCryptoError : Throwable() { data class OlmError(val olmException: OlmException) : MXCryptoError() - data class UnknownDevice(val deviceList: MXUsersDevicesMap) : MXCryptoError() + data class UnknownDevice(val deviceList: MXUsersDevicesMap) : MXCryptoError() enum class ErrorType { ENCRYPTING_NOT_ENABLED, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt new file mode 100644 index 0000000000..b01f42cd84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2020 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.matrix.android.api.session.crypto.crosssigning + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult +import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth + +interface CrossSigningService { + + fun isCrossSigningEnabled(): Boolean + + fun isUserTrusted(otherUserId: String): Boolean + + /** + * Will not force a download of the key, but will verify signatures trust chain. + * Checks that my trusted user key has signed the other user UserKey + */ + fun checkUserTrust(otherUserId: String): UserTrustResult + + /** + * Initialize cross signing for this user. + * Users needs to enter credentials + */ + fun initializeCrossSigning(authParams: UserPasswordAuth?, + callback: MatrixCallback? = null) + + fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? + + fun getLiveCrossSigningKeys(userId: String): LiveData> + + fun getMyCrossSigningKeys(): MXCrossSigningInfo? + + fun canCrossSign(): Boolean + + fun trustUser(otherUserId: String, + callback: MatrixCallback) + + /** + * Sign one of your devices and upload the signature + */ + fun signDevice(deviceId: String, + callback: MatrixCallback) + + fun checkDeviceTrust(otherUserId: String, + otherDeviceId: String, + locallyTrusted: Boolean?): DeviceTrustResult +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/MXCrossSigningInfo.kt new file mode 100644 index 0000000000..979ff16d55 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/MXCrossSigningInfo.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.matrix.android.api.session.crypto.crosssigning + +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.KeyUsage + +data class MXCrossSigningInfo( + + var userId: String, + + var crossSigningKeys: List = ArrayList() + +) { + + fun isTrusted() : Boolean = masterKey()?.trustLevel?.isVerified() == true + && selfSigningKey()?.trustLevel?.isVerified() == true + + fun masterKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.MASTER.value) == true } + + fun userKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.USER_SIGNING.value) == true } + + fun selfSigningKey(): CryptoCrossSigningKey? = crossSigningKeys + .firstOrNull { it.usages?.contains(KeyUsage.SELF_SIGNING.value) == true } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt index 92a69bcad6..79448de83f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// TODO Rename package package im.vector.matrix.android.api.session.crypto.sas enum class CancelCode(val value: String, val humanReadable: String) { @@ -25,7 +27,9 @@ enum class CancelCode(val value: String, val humanReadable: String) { UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"), InvalidMessage("m.invalid_message", "an invalid message was received"), MismatchedKeys("m.key_mismatch", "Key mismatch"), - UserMismatchError("m.user_error", "User mismatch") + UserError("m.user_error", "User error"), + MismatchedUser("m.user_mismatch", "User mismatch"), + QrCodeInvalid("m.qr_code.invalid", "Invalid QR code") } fun safeValueOf(code: String?): CancelCode { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt index 57dfc74236..8e349416dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.crypto.sas -interface IncomingSasVerificationTransaction { +interface IncomingSasVerificationTransaction : SasVerificationTransaction { val uxState: UxState fun performAccept() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt index f2c16da997..7ab386295a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationTransaction.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.crypto.sas -interface OutgoingSasVerificationRequest { +interface OutgoingSasVerificationTransaction : SasVerificationTransaction { val uxState: UxState enum class UxState { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt new file mode 100644 index 0000000000..b2b6cfdd39 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2020 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.matrix.android.api.session.crypto.sas + +interface QrCodeVerificationTransaction : VerificationTransaction { + + /** + * To use to display a qr code, for the other user to scan it + */ + val qrCodeText: String? + + /** + * Call when you have scan the other user QR code + */ + fun userHasScannedOtherQrCode(otherQrCodeText: String) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt index 1f7bef558f..912e2b65e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt @@ -16,18 +16,7 @@ package im.vector.matrix.android.api.session.crypto.sas -interface SasVerificationTransaction { - var state: SasVerificationTxState - - val cancelledReason: CancelCode? - - val transactionId: String - - val otherUserId: String - - var otherDeviceId: String? - - val isIncoming: Boolean +interface SasVerificationTransaction : VerificationTransaction { fun supportsEmoji(): Boolean @@ -37,11 +26,6 @@ interface SasVerificationTransaction { fun getDecimalCodeRepresentation(): String - /** - * User wants to cancel the transaction - */ - fun cancel() - /** * To be called by the client when the user has verified that * both short codes do match diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt index a2dd90bc84..b8f0f23891 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt @@ -17,10 +17,13 @@ package im.vector.matrix.android.api.session.crypto.sas /** - * Verification methods supported (or to be supported) by the matrix SDK + * Verification methods */ enum class VerificationMethod { + // Use it when your application supports the SAS verification method SAS, - // Not supported yet - SCAN + // Use it if your application is able to display QR codes + QR_CODE_SHOW, + // Use it if your application is able to scan QR codes + QR_CODE_SCAN } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt similarity index 66% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt index a626dd5573..a491014682 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt @@ -17,44 +17,50 @@ package im.vector.matrix.android.api.session.crypto.sas import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest /** * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework * * Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors. - * SAS verification is a user-friendly key verification process. - * SAS verification is intended to be a highly interactive process for users, + * Verification is a user-friendly key verification process. + * Verification is intended to be a highly interactive process for users, * and as such exposes verification methods which are easier for users to use. */ -interface SasVerificationService { +interface VerificationService { - fun addListener(listener: SasVerificationListener) + fun addListener(listener: VerificationListener) - fun removeListener(listener: SasVerificationListener) + fun removeListener(listener: VerificationListener) /** * Mark this device as verified manually */ fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) - fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? + fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? - fun getExistingVerificationRequest(otherUser: String): List? + fun getExistingVerificationRequest(otherUserId: String): List? - fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? + fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? - fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String? + fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String? /** * Request a key verification from another user using toDevice events. */ - fun requestKeyVerificationInDMs(methods: List, userId: String, roomId: String): PendingVerificationRequest + fun requestKeyVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + localId: String? = LocalEcho.createLocalEchoId() + ): PendingVerificationRequest fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) + // Only SAS method is supported for the moment fun beginKeyVerificationInDMs(method: VerificationMethod, transactionId: String, roomId: String, @@ -65,13 +71,16 @@ interface SasVerificationService { /** * Returns false if the request is unknown */ - fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean + fun readyPendingVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + transactionId: String): Boolean // fun transactionUpdated(tx: SasVerificationTransaction) - interface SasVerificationListener { - fun transactionCreated(tx: SasVerificationTransaction) - fun transactionUpdated(tx: SasVerificationTransaction) + interface VerificationListener { + fun transactionCreated(tx: VerificationTransaction) + fun transactionUpdated(tx: VerificationTransaction) fun markedAsManuallyVerified(userId: String, deviceId: String) {} fun verificationRequestCreated(pr: PendingVerificationRequest) {} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt new file mode 100644 index 0000000000..16bb37e180 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTransaction.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 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.matrix.android.api.session.crypto.sas + +interface VerificationTransaction { + + var state: VerificationTxState + + val cancelledReason: CancelCode? + val transactionId: String + val otherUserId: String + var otherDeviceId: String? + + // TODO Not used. Remove? + val isIncoming: Boolean + /** + * User wants to cancel the transaction + */ + fun cancel() + + fun cancel(code: CancelCode) + + fun isToDeviceTransport(): Boolean +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt similarity index 93% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt index 350ec2c381..8d4ebb5549 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.api.session.crypto.sas -enum class SasVerificationTxState { +enum class VerificationTxState { None, // I have started a verification request SendingStart, @@ -44,6 +44,8 @@ enum class SasVerificationTxState { Verified, // Global: The verification has been cancelled (by me or other), see cancelReason for details + // When I do the cancel Cancelled, + // When the other user do a cancel OnCancelled } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt index dffa4ba3cb..3031b213d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt @@ -20,8 +20,9 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.SasMode import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent -import im.vector.matrix.android.internal.crypto.model.rest.supportedVerificationMethods -import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber @@ -34,7 +35,8 @@ internal data class MessageVerificationStartContent( @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List?, @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, @Json(name = "method") override val method: String?, - @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "secret") override val sharedSecret: String? ) : VerificationInfoStart { override fun toCanonicalJson(): String? { @@ -44,22 +46,39 @@ internal data class MessageVerificationStartContent( override val transactionID: String? get() = relatesTo?.eventId + // TODO Move those method to the interface? override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() - || method !in supportedVerificationMethods - || keyAgreementProtocols.isNullOrEmpty() - || hashes.isNullOrEmpty() - || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() - || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) - && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) - || shortAuthenticationStrings.isNullOrEmpty() - || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { + || (method == VERIFICATION_METHOD_SAS && !isValidSas()) + || (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) { Timber.e("## received invalid verification request") return false } return true } + private fun isValidSas(): Boolean { + if (keyAgreementProtocols.isNullOrEmpty() + || hashes.isNullOrEmpty() + || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() + || (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256) + && !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) + || shortAuthenticationStrings.isNullOrEmpty() + || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { + return false + } + + return true + } + + private fun isValidReciprocate(): Boolean { + if (sharedSecret.isNullOrBlank()) { + return false + } + + return true + } + override fun toEventContent() = toContent() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt index c045019cda..a3b0a567fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt @@ -30,3 +30,7 @@ const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2" * Matrix algorithm value for megolm keys backup. */ const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" + +// TODO Refacto: use this constants everywhere +const val ed25519 = "ed25519" +const val curve25519 = "curve25519" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 22ccf44983..5ffa4b2166 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -21,14 +21,68 @@ import dagger.Module import dagger.Provides import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi -import im.vector.matrix.android.internal.crypto.keysbackup.tasks.* +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteBackupTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultDeleteSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupLastVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultGetSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultStoreSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DefaultUpdateKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteBackupTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule -import im.vector.matrix.android.internal.crypto.tasks.* +import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice +import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultDeleteDeviceWithUserPasswordTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultDownloadKeysForUsers +import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadKeysTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadSignaturesTask +import im.vector.matrix.android.internal.crypto.tasks.DefaultUploadSigningKeysTask +import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask +import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask +import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask +import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask +import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask +import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask +import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask +import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.SessionFilesDirectory @@ -132,6 +186,12 @@ internal abstract class CryptoModule { @Binds abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask + @Binds + abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask + + @Binds + abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask + @Binds abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask @@ -193,4 +253,7 @@ internal abstract class CryptoModule { @Binds abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask) : DeleteDeviceWithUserPasswordTask + + @Binds + abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index bf981cc673..1ceb45939c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -21,6 +21,7 @@ package im.vector.matrix.android.internal.crypto import android.content.Context import android.os.Handler import android.os.Looper +import androidx.lifecycle.LiveData import com.squareup.moshi.Types import com.zhuinden.monarchy.Monarchy import dagger.Lazy @@ -44,7 +45,10 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult @@ -54,10 +58,16 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.toRest import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.* -import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask +import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask +import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask +import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask +import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask +import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask +import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MoshiProvider @@ -71,7 +81,12 @@ import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.fetchCopied -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -111,8 +126,10 @@ internal class DefaultCryptoService @Inject constructor( private val oneTimeKeysUploader: OneTimeKeysUploader, // private val roomDecryptorProvider: RoomDecryptorProvider, - // The SAS verification service. - private val sasVerificationService: DefaultSasVerificationService, + // The verification service. + private val verificationService: DefaultVerificationService, + + private val crossSigningService: DefaultCrossSigningService, // private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, // @@ -139,7 +156,7 @@ internal class DefaultCryptoService @Inject constructor( ) : CryptoService { init { - sasVerificationService.cryptoService = this + verificationService.cryptoService = this } private val uiHandler = Handler(Looper.getMainLooper()) @@ -168,7 +185,17 @@ internal class DefaultCryptoService @Inject constructor( override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { - this.callback = callback + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + // bg refresh of crypto device + downloadKeys(listOf(credentials.userId), true, object : MatrixCallback> {}) + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + } } .executeBy(taskExecutor) } @@ -193,7 +220,7 @@ internal class DefaultCryptoService @Inject constructor( return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version } - override fun getMyDevice(): MXDeviceInfo { + override fun getMyDevice(): CryptoDeviceInfo { return myDeviceInfoHolder.get().myDevice } @@ -313,9 +340,11 @@ internal class DefaultCryptoService @Inject constructor( override fun getKeysBackupService() = keysBackup /** - * @return the SasVerificationService + * @return the VerificationService */ - override fun getSasVerificationService() = sasVerificationService + override fun getVerificationService() = verificationService + + override fun getCrossSigningService() = crossSigningService /** * A sync response has been received @@ -349,7 +378,7 @@ internal class DefaultCryptoService @Inject constructor( * @param algorithm the encryption algorithm. * @return the device info, or null if not found / unsupported algorithm / crypto released */ - override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? { + override fun deviceWithIdentityKey(senderKey: String, algorithm: String): CryptoDeviceInfo? { return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) { // We only deal in olm keys null @@ -362,13 +391,20 @@ internal class DefaultCryptoService @Inject constructor( * @param userId the user id * @param deviceId the device id */ - override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? { + override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? { return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) { - cryptoStore.getUserDevice(deviceId, userId) + cryptoStore.getUserDevice(userId, deviceId) } else { null } } + override fun getCryptoDeviceInfo(userId: String): List { + return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList() + } + + override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { + return cryptoStore.getLiveDeviceList(userId) + } /** * Set the devices as known @@ -393,7 +429,7 @@ internal class DefaultCryptoService @Inject constructor( // assume if the device is either verified or blocked // it means that the device is known if (device?.isUnknown == true) { - device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED + device.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) isUpdated = true } } @@ -410,12 +446,12 @@ internal class DefaultCryptoService @Inject constructor( /** * Update the blocked/verified state of the given device. * - * @param verificationStatus the new verification status - * @param deviceId the unique identifier for the device. - * @param userId the owner of the device + * @param trustLevel the new trust level + * @param userId the owner of the device + * @param deviceId the unique identifier for the device. */ - override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String) { - setDeviceVerificationAction.handle(verificationStatus, deviceId, userId) + override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + setDeviceVerificationAction.handle(trustLevel, userId, deviceId) } /** @@ -494,9 +530,8 @@ internal class DefaultCryptoService @Inject constructor( /** * @return the stored device keys for a user. */ - override fun getUserDevices(userId: String): MutableList { - val map = cryptoStore.getUserDevices(userId) - return if (null != map) ArrayList(map.values) else ArrayList() + override fun getUserDevices(userId: String): MutableList { + return cryptoStore.getUserDevices(userId)?.values?.toMutableList() ?: ArrayList() } fun isEncryptionEnabledForInvitedUser(): Boolean { @@ -758,11 +793,15 @@ internal class DefaultCryptoService @Inject constructor( // Prepare the device keys data to send // Sign it val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) - getMyDevice().signatures = objectSigner.signObject(canonicalJson) + var rest = getMyDevice().toRest() + + rest = rest.copy( + signatures = objectSigner.signObject(canonicalJson) + ) // For now, we set the device id explicitly, as we may not be using the // same one as used in login. - val uploadDeviceKeysParams = UploadKeysTask.Params(getMyDevice().toDeviceKeys(), null, getMyDevice().deviceId) + val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null, getMyDevice().deviceId) return uploadKeysTask.execute(uploadDeviceKeysParams) } @@ -1002,8 +1041,8 @@ internal class DefaultCryptoService @Inject constructor( * @param devicesInRoom the devices map * @return the unknown devices map */ - private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { - val unknownDevices = MXUsersDevicesMap() + private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { + val unknownDevices = MXUsersDevicesMap() val userIds = devicesInRoom.userIds for (userId in userIds) { devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId -> @@ -1018,7 +1057,7 @@ internal class DefaultCryptoService @Inject constructor( return unknownDevices } - override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { + override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { deviceListManager.downloadKeys(userIds, forceDownload) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index b2002f0916..da97520eec 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -19,12 +19,15 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoInfoMapper import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.sync.SyncTokenStore +import okhttp3.internal.toImmutableList import timber.log.Timber import javax.inject.Inject @@ -36,6 +39,30 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM private val credentials: Credentials, private val downloadKeysForUsersTask: DownloadKeysForUsersTask) { + interface UserDevicesUpdateListener { + fun onUsersDeviceUpdate(users: List) + } + + private val deviceChangeListeners = ArrayList() + + fun addListener(listener: UserDevicesUpdateListener) { + deviceChangeListeners.add(listener) + } + + fun removeListener(listener: UserDevicesUpdateListener) { + deviceChangeListeners.remove(listener) + } + + fun dispatchDeviceChange(users: List) { + deviceChangeListeners.forEach { + try { + it.onUsersDeviceUpdate(users) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to dispatch device chande") + } + } + } + // HS not ready for retry private val notReadyToRetryHS = mutableSetOf() @@ -166,13 +193,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param userIds the userIds list * @param failures the failure map. */ - private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { + private fun onKeysDownloadSucceed(userIds: List, failures: Map>?): MXUsersDevicesMap { if (failures != null) { for ((k, value) in failures) { val statusCode = when (val status = value["status"]) { is Double -> status.toInt() - is Int -> status.toInt() - else -> 0 + is Int -> status.toInt() + else -> 0 } if (statusCode == 503) { synchronized(notReadyToRetryHS) { @@ -182,7 +209,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } } val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() - val usersDevicesInfoMap = MXUsersDevicesMap() + val usersDevicesInfoMap = MXUsersDevicesMap() for (userId in userIds) { val devices = cryptoStore.getUserDevices(userId) if (null == devices) { @@ -207,6 +234,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } } cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + + dispatchDeviceChange(userIds.toImmutableList()) return usersDevicesInfoMap } @@ -217,10 +246,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param userIds The users to fetch. * @param forceDownload Always download the keys even if cached. */ - suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { + suspend fun downloadKeys(userIds: List?, forceDownload: Boolean): MXUsersDevicesMap { Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") // Map from userId -> deviceId -> MXDeviceInfo - val stored = MXUsersDevicesMap() + val stored = MXUsersDevicesMap() // List of user ids we need to download keys for val downloadUsers = ArrayList() @@ -265,7 +294,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * * @param downloadUsers the user ids list */ - private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList): MXUsersDevicesMap { + private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList): MXUsersDevicesMap { Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") // get the user ids which did not already trigger a keys download val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } @@ -283,39 +312,62 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") for (userId in filteredUsers) { - val devices = response.deviceKeys?.get(userId) - Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices") - if (devices != null) { - val mutableDevices = devices.toMutableMap() - for ((deviceId, deviceInfo) in devices) { + // al devices = + val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } + ?.toMutableMap() + + Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models") + if (!models.isNullOrEmpty()) { + for ((deviceId, deviceInfo) in models) { // Get the potential previously store device keys for this device - val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId) + val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId) // in some race conditions (like unit tests) // the self device must be seen as verified if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) { - deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED + deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true) } // Validate received keys if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { // New device keys are not valid. Do not store them - mutableDevices.remove(deviceId) + models.remove(deviceId) if (null != previouslyStoredDeviceKeys) { // But keep old validated ones if any - mutableDevices[deviceId] = previouslyStoredDeviceKeys + models[deviceId] = previouslyStoredDeviceKeys } } else if (null != previouslyStoredDeviceKeys) { // The verified status is not sync'ed with hs. // This is a client side information, valid only for this client. // So, transfer its previous value - mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified + models[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel } } // Update the store // Note that devices which aren't in the response will be removed from the stores - cryptoStore.storeUserDevices(userId, mutableDevices) + cryptoStore.storeUserDevices(userId, models) } + + // Handle cross signing keys update + val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { + Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + } + val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + } + val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { + Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + } + cryptoStore.storeUserCrossSigningKeys( + userId, + masterKey, + selfSigningKey, + userSigningKey + ) } + + // Update devices trust for these users + dispatchDeviceChange(downloadUsers) + return onKeysDownloadSucceed(filteredUsers, response.failures) } @@ -329,7 +381,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM * @param previouslyStoredDeviceKeys the device keys we received before for this device * @return true if succeeds */ - private fun validateDeviceKeys(deviceKeys: MXDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: MXDeviceInfo?): Boolean { + private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { if (null == deviceKeys) { Timber.e("## validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") return false @@ -357,14 +409,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } val signKeyId = "ed25519:" + deviceKeys.deviceId - val signKey = deviceKeys.keys?.get(signKeyId) + val signKey = deviceKeys.keys[signKeyId] if (null == signKey) { Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") return false } - val signatureMap = deviceKeys.signatures?.get(userId) + val signatureMap = deviceKeys.signatures[userId] if (null == signatureMap) { Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index e8d8bf0f35..e7c500edbf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -107,7 +107,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( cryptoStore.deleteIncomingRoomKeyRequest(request) } // if the device is verified already, share the keys - val device = cryptoStore.getUserDevice(deviceId!!, userId) + val device = cryptoStore.getUserDevice(userId, deviceId!!) if (device != null) { if (device.isVerified) { Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 6171b32811..75a4ba5ed7 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -141,6 +141,7 @@ internal class MXOlmDevice @Inject constructor( */ fun release() { olmAccount?.releaseAccount() + olmUtility?.releaseUtility() } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt index f93245de12..e50faf6f76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MyDeviceInfoHolder.kt @@ -17,7 +17,8 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope import javax.inject.Inject @@ -35,11 +36,13 @@ internal class MyDeviceInfoHolder @Inject constructor( /** * my device info */ - val myDevice: MXDeviceInfo = MXDeviceInfo(credentials.deviceId!!, credentials.userId) + val myDevice: CryptoDeviceInfo init { + val keys = HashMap() +// TODO it's a bit strange, why not load from DB? if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) { keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!! } @@ -48,10 +51,22 @@ internal class MyDeviceInfoHolder @Inject constructor( keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!! } - myDevice.keys = keys +// myDevice.keys = keys +// +// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() - myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() - myDevice.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED + // TODO hwo to really check cross signed status? + // + val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false +// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true) + + myDevice = CryptoDeviceInfo( + credentials.deviceId!!, + credentials.userId, + keys = keys, + algorithms = MXCryptoAlgorithms.supportedAlgorithms(), + trustLevel = DeviceTrustLevel(crossSigned, true) + ) // Add our own deviceinfo to the store val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 0283d3c85b..e1cac0d75f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.actions import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap @@ -28,8 +28,8 @@ import javax.inject.Inject internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { - suspend fun handle(devicesByUser: Map>): MXUsersDevicesMap { - val devicesWithoutSession = ArrayList() + suspend fun handle(devicesByUser: Map>): MXUsersDevicesMap { + val devicesWithoutSession = ArrayList() val results = MXUsersDevicesMap() @@ -102,7 +102,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val return results } - private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? { + private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? { var sessionId: String? = null val deviceId = deviceInfo.deviceId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt index 0c649cce89..5766ee9980 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt @@ -40,7 +40,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o // Don't bother setting up session to ourself it.identityKey() != olmDevice.deviceCurve25519Key // Don't bother setting up sessions with blocked users - && !it.isVerified + && !(it.trustLevel?.isVerified() ?: false) } } return ensureOlmSessionsForDevicesAction.handle(devicesByUser) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt index ebe219600d..fae205e581 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MessageEncrypter.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.crypto.actions import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM import im.vector.matrix.android.internal.crypto.MXOlmDevice -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.convertToUTF8 @@ -37,7 +37,7 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre * @param deviceInfos list of device infos to encrypt for. * @return the content for an m.room.encrypted event. */ - fun encryptMessage(payloadFields: Map, deviceInfos: List): EncryptedMessage { + fun encryptMessage(payloadFields: Map, deviceInfos: List): EncryptedMessage { val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } val payloadJson = payloadFields.toMutableMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index 2d0c77c768..8dad832617 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.crypto.actions +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.di.UserId @@ -27,8 +28,8 @@ internal class SetDeviceVerificationAction @Inject constructor( @UserId private val userId: String, private val keysBackup: KeysBackup) { - fun handle(verificationStatus: Int, deviceId: String, userId: String) { - val device = cryptoStore.getUserDevice(deviceId, userId) + fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + val device = cryptoStore.getUserDevice(userId, deviceId) // Sanity check if (null == device) { @@ -36,10 +37,7 @@ internal class SetDeviceVerificationAction @Inject constructor( return } - if (device.verified != verificationStatus) { - device.verified = verificationStatus - cryptoStore.storeUserDevice(userId, device) - + if (device.isVerified != trustLevel.isVerified()) { if (userId == this.userId) { // If one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on @@ -47,5 +45,10 @@ internal class SetDeviceVerificationAction @Inject constructor( keysBackup.checkAndStartKeysBackup() } } + + if (device.trustLevel != trustLevel) { + device.trustLevel = trustLevel + cryptoStore.storeUserDevice(userId, device) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 81ac1403df..49871434f9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -297,7 +297,7 @@ internal class MXMegolmDecryption(private val userId: String, runCatching { deviceListManager.downloadKeys(listOf(userId), false) } .mapCatching { val deviceId = request.deviceId - val deviceInfo = cryptoStore.getUserDevice(deviceId ?: "", userId) + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") if (deviceInfo == null) { throw RuntimeException() } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 897a1f0a5d..ee35810763 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -95,7 +95,7 @@ internal class MXMegolmEncryption( * * @param devicesInRoom the devices list */ - private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { + private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap): MXOutboundSessionInfo { var session = outboundSession if (session == null // Need to make a brand new session? @@ -106,7 +106,7 @@ internal class MXMegolmEncryption( outboundSession = session } val safeSession = session - val shareMap = HashMap>()/* userId */ + val shareMap = HashMap>()/* userId */ val userIds = devicesInRoom.userIds for (userId in userIds) { val deviceIds = devicesInRoom.getUserDeviceIds(userId) @@ -129,14 +129,14 @@ internal class MXMegolmEncryption( * @param devicesByUsers the devices map */ private suspend fun shareKey(session: MXOutboundSessionInfo, - devicesByUsers: Map>) { + devicesByUsers: Map>) { // nothing to send, the task is done if (devicesByUsers.isEmpty()) { Timber.v("## shareKey() : nothing more to do") return } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) - val subMap = HashMap>() + val subMap = HashMap>() var devicesCount = 0 for ((userId, devices) in devicesByUsers) { subMap[userId] = devices @@ -158,7 +158,7 @@ internal class MXMegolmEncryption( * @param devicesByUser the devices map */ private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo, - devicesByUser: Map>) { + devicesByUser: Map>) { val sessionKey = olmDevice.getSessionKey(session.sessionId) val chainIndex = olmDevice.getMessageIndex(session.sessionId) @@ -262,7 +262,7 @@ internal class MXMegolmEncryption( * * @param userIds the user ids whose devices must be checked. */ - private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { + private suspend fun getDevicesInRoom(userIds: List): MXUsersDevicesMap { // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via @@ -271,8 +271,8 @@ internal class MXMegolmEncryption( val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() || cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) - val devicesInRoom = MXUsersDevicesMap() - val unknownDevices = MXUsersDevicesMap() + val devicesInRoom = MXUsersDevicesMap() + val unknownDevices = MXUsersDevicesMap() for (userId in keys.userIds) { val deviceIds = keys.getUserDeviceIds(userId) ?: continue diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt index 5ab272d4e2..47ec98e5c9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import timber.log.Timber @@ -52,7 +52,7 @@ internal class MXOutboundSessionInfo( * @param devicesInRoom the devices map * @return true if we have shared the session with devices which aren't in devicesInRoom. */ - fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { + fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { val userIds = sharedWithDevices.userIds for (userId in userIds) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt index 1c275940af..899e884e0d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -25,7 +25,7 @@ import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore internal class MXOlmEncryption( @@ -42,7 +42,7 @@ internal class MXOlmEncryption( // // TODO: there is a race condition here! What if a new user turns up ensureSession(userIds) - val deviceInfos = ArrayList() + val deviceInfos = ArrayList() for (userId in userIds) { val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() for (device in devices) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt index b2e880c2f3..4953d53ae0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -65,6 +65,33 @@ internal interface CryptoApi { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") fun downloadKeysForUsers(@Body params: KeysQueryBody): Call + /** + * CrossSigning - Uploading signing keys + * Public keys for the cross-signing keys are uploaded to the servers using /keys/device_signing/upload. + * This endpoint requires UI Auth. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/device_signing/upload") + fun uploadSigningKeys(@Body params: UploadSigningKeysBody): Call + + /** + * CrossSigning - Uploading signatures + * Signatures of device keys can be up + * loaded using /keys/signatures/upload. + * For example, Alice signs one of her devices (HIJKLMN) (using her self-signing key), + * her own master key (using her HIJKLMN device), Bob's master key (using her user-signing key). + * + * The response contains a failures property, which is a map of user ID to device ID to failure reason, if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + * + * After Alice uploads a signature for her own devices or master key, + * her signature will be included in the results of the /keys/query request when anyone requests her keys. + * However, signatures made for other users' keys, made by her user-signing key, will not be included. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "keys/signatures/upload") + fun uploadSignatures(@Body params: Map?): Call + /** * Claim one-time keys. * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt new file mode 100644 index 0000000000..cbde0dc046 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -0,0 +1,630 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.crosssigning + +import androidx.lifecycle.LiveData +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.KeyUsage +import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask +import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.TaskConstraints +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.JsonCanonicalizer +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.withoutPrefix +import kotlinx.coroutines.CoroutineScope +import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class DefaultCrossSigningService @Inject constructor( + @UserId private val userId: String, + private val cryptoStore: IMXCryptoStore, + private val myDeviceInfoHolder: Lazy, + private val olmDevice: MXOlmDevice, + private val deviceListManager: DeviceListManager, + private val uploadSigningKeysTask: UploadSigningKeysTask, + private val uploadSignaturesTask: UploadSignaturesTask, + private val cryptoCoroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor) : 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 -> + Timber.i("## CrossSigning - Found Existing self signed keys") + Timber.i("## CrossSigning - Checking if private keys are known") + + cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> + privateKeysInfo.master + ?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { + masterPkSigning = pkSigning + Timber.i("## CrossSigning - Loading master key success") + } else { + Timber.w("## CrossSigning - Public master key does not match the private key") + // TODO untrust + } + } + privateKeysInfo.user + ?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading User Signing key success") + } else { + Timber.w("## CrossSigning - Public User key does not match the private key") + // TODO untrust + } + } + privateKeysInfo.selfSigned + ?.fromBase64NoPadding() + ?.let { privateKeySeed -> + val pkSigning = OlmPkSigning() + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading Self Signing key success") + } else { + Timber.w("## CrossSigning - Public Self Signing key does not match the private key") + // TODO untrust + } + } + } + + // Recover local trust in case private key are there? + cryptoStore.setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified()) + } + } catch (e: Throwable) { + // Mmm this kind of a big issue + Timber.e(e, "Failed to initialize Cross Signing") + } + + deviceListManager.addListener(this) + } + + fun release() { + olmUtility?.releaseUtility() + listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } + deviceListManager.removeListener(this) + } + + protected fun finalize() { + release() + } + + /** + * - Make 3 key pairs (MSK, USK, SSK) + * - Save the private keys with proper security + * - Sign the keys and upload them + * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures + */ + override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback?) { + Timber.d("## CrossSigning initializeCrossSigning") + + // ================= + // MASTER KEY + // ================= + val masterPkOlm = OlmPkSigning() + val masterKeyPrivateKey = OlmPkSigning.generateSeed() + val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey) + + Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey") + + // ================= + // USER KEY + // ================= + val userSigningPkOlm = OlmPkSigning() + val uskPrivateKey = OlmPkSigning.generateSeed() + val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey) + + Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey") + + // Sign userSigningKey with master + val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .build() + .canonicalSignable() + .let { masterPkOlm.sign(it) } + + // ================= + // SELF SIGNING KEY + // ================= + val selfSigningPkOlm = OlmPkSigning() + val sskPrivateKey = OlmPkSigning.generateSeed() + val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey) + + Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey") + + // Sign userSigningKey with master + val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .build().signalableJSONDictionary()).let { masterPkOlm.sign(it) } + + // I need to upload the keys + val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER) + .key(masterPublicKey) + .build() + val params = UploadSigningKeysTask.Params( + masterKey = mskCrossSigningKeyInfo, + userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING) + .key(uskPublicKey) + .signature(userId, masterPublicKey, signedUSK) + .build(), + selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING) + .key(sskPublicKey) + .signature(userId, masterPublicKey, signedSSK) + .build(), + userPasswordAuth = authParams + ) + + this.masterPkSigning = masterPkOlm + this.userPkSigning = userSigningPkOlm + this.selfSigningPkSigning = selfSigningPkOlm + + val crossSigningInfo = MXCrossSigningInfo(userId, listOf(params.masterKey, params.userKey, params.selfSignedKey)) + cryptoStore.setMyCrossSigningInfo(crossSigningInfo) + cryptoStore.setUserKeysAsTrusted(userId) + cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding()) + + uploadSigningKeysTask.configureWith(params) { + this.constraints = TaskConstraints(true) + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.i("## CrossSigning - Keys successfully uploaded") + + // Sign the current device with SSK + val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() + + val myDevice = myDeviceInfoHolder.get().myDevice + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) + val signedDevice = selfSigningPkOlm.sign(canonicalJson) + val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()).also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice) + } + myDevice.copy(signatures = updateSignatures).let { + uploadSignatureQueryBuilder.withDeviceInfo(it) + } + + // sign MSK with device key (migration) and upload signatures + olmDevice.signMessage(JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()))?.let { sign -> + val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap() + ?: HashMap()).also { + it[userId] = (it[userId] + ?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign) + } + mskCrossSigningKeyInfo.copy( + signatures = mskUpdatedSignatures + ).let { + uploadSignatureQueryBuilder.withSigningKeyInfo(it) + } + } + + resetTrustOnKeyChange() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) { + // this.retryCount = 3 + this.constraints = TaskConstraints(true) + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.i("## CrossSigning - signatures successfully uploaded") + callback?.onSuccess(Unit) + } + + override fun onFailure(failure: Throwable) { + // Clear + Timber.e(failure, "## CrossSigning - Failed to upload signatures") + clearSigningKeys() + } + } + }.executeBy(taskExecutor) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## CrossSigning - Failed to upload signing keys") + clearSigningKeys() + callback?.onFailure(failure) + } + } + }.executeBy(taskExecutor) + } + + private fun clearSigningKeys() { + this@DefaultCrossSigningService.masterPkSigning?.releaseSigning() + this@DefaultCrossSigningService.userPkSigning?.releaseSigning() + this@DefaultCrossSigningService.selfSigningPkSigning?.releaseSigning() + + this@DefaultCrossSigningService.masterPkSigning = null + this@DefaultCrossSigningService.userPkSigning = null + this@DefaultCrossSigningService.selfSigningPkSigning = null + + cryptoStore.setMyCrossSigningInfo(null) + cryptoStore.storePrivateKeysInfo(null, null, null) + } + + private fun resetTrustOnKeyChange() { + Timber.i("## CrossSigning - Clear all other user trust") + cryptoStore.clearOtherUserTrust() + } + + /** + * + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶ MSK + * │ + * │ │ + * │ SSK │ + * │ │ + * │ │ + * └──▶ USK ────────────┘ + */ + override fun isUserTrusted(otherUserId: String): Boolean { + return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true + } + + override fun isCrossSigningEnabled(): Boolean { + return checkSelfTrust().isVerified() + } + + /** + * Will not force a download of the key, but will verify signatures trust chain + */ + override fun checkUserTrust(otherUserId: String): UserTrustResult { + Timber.d("## CrossSigning checkUserTrust for $otherUserId") + if (otherUserId == userId) { + return checkSelfTrust() + } + // I trust a user if I trust his master key + // I can trust the master key if it is signed by my user key + // TODO what if the master key is signed by a device key that i have verified + + // First let's get my user key + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myUserKey = myCrossSigningInfo?.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + if (!myCrossSigningInfo.isTrusted()) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + // Let's get the other user master key + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) + + val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") + + if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") + return UserTrustResult.KeyNotSigned(otherMasterKey) + } + + // Check that Alice USK signature of Bob MSK is valid + try { + olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey) + } + + return UserTrustResult.Success + } + + private fun checkSelfTrust(): UserTrustResult { + // Special case when it's me, + // I have to check that MSK -> USK -> SSK + // and that MSK is trusted (i know the private key, or is signed by a trusted device) + val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + + val myMasterKey = myCrossSigningInfo?.masterKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + // Is the master key trusted + // 1) check if I know the private key + val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() + ?.master + ?.fromBase64NoPadding() + + var isMaterKeyTrusted = false + if (masterPrivateKey != null) { + // Check if private match public + var olmPkSigning: OlmPkSigning? = null + try { + olmPkSigning = OlmPkSigning() + val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) + isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK + } catch (failure: Throwable) { + Timber.e(failure) + } + olmPkSigning?.releaseSigning() + } else { + // Maybe it's signed by a locally trusted device? + myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> + val potentialDeviceId = key.withoutPrefix("ed25519:") + val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) + if (potentialDevice != null && potentialDevice.isVerified) { + // Check signature validity? + try { + olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable()) + isMaterKeyTrusted = true + return@forEach + } catch (failure: Throwable) { + // log + Timber.v(failure) + } + } + } + } + + if (!isMaterKeyTrusted) { + return UserTrustResult.KeysNotTrusted(myCrossSigningInfo) + } + + val myUserKey = myCrossSigningInfo.userKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK") + return UserTrustResult.KeyNotSigned(myUserKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey) + } + + val mySSKey = myCrossSigningInfo.selfSigningKey() + ?: return UserTrustResult.CrossSigningNotConfigured(userId) + + val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures + ?.get(userId) // Signatures made by me + ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}") + + if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) { + Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK") + return UserTrustResult.KeyNotSigned(mySSKey) + } + + // Check that Alice USK signature of Alice MSK is valid + try { + olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable()) + } catch (failure: Throwable) { + return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey) + } + + return UserTrustResult.Success + } + + override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? { + return cryptoStore.getCrossSigningInfo(otherUserId) + } + + override fun getLiveCrossSigningKeys(userId: String): LiveData> { + return cryptoStore.getLiveCrossSigningInfo(userId) + } + + override fun getMyCrossSigningKeys(): MXCrossSigningInfo? { + return cryptoStore.getMyCrossSigningInfo() + } + + override fun canCrossSign(): Boolean { + return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + } + + override fun trustUser(otherUserId: String, callback: MatrixCallback) { + Timber.d("## CrossSigning - Mark user $userId as trusted ") + // We should have this user keys + val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() + if (otherMasterKeys == null) { + callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) + return + } + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + return + } + val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey + if (userPubKey == null || userPkSigning == null) { + callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) + return + } + + // Sign the other MasterKey with our UserSigning key + val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, + otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("## CrossSigning - Failed to sign")) + return + } + + cryptoStore.setUserKeysAsTrusted(otherUserId, true) + // TODO update local copy with new signature directly here? kind of local echo of trust? + + Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + val uploadQuery = UploadSignatureQueryBuilder() + .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.callback = callback + }.executeBy(taskExecutor) + } + + override fun signDevice(deviceId: String, callback: MatrixCallback) { + // This device should be yours + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device == null) { + callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) + return + } + + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("CrossSigning is not setup for this account")) + return + } + + val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey + if (ssPubKey == null || selfSigningPkSigning == null) { + callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) + return + } + + // Sign with self signing + val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("Failed to sign")) + return + } + val toUpload = device.copy( + signatures = mapOf( + userId + to + mapOf( + "ed25519:$ssPubKey" to newSignature + ) + ) + ) + + val uploadQuery = UploadSignatureQueryBuilder() + .withDeviceInfo(toUpload) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.callback = callback + }.executeBy(taskExecutor) + } + + override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { + val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) + ?: return DeviceTrustResult.UnknownDevice(otherDeviceId) + + val myKeys = getUserCrossSigningKeys(userId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + + if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) + + val otherKeys = getUserCrossSigningKeys(otherUserId) + ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId)) + + // TODO should we force verification ? + if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) + + // Check if the trust chain is valid + /* + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶MSK + * │ + * │ │ │ + * │ SSK │ └──▶ SSK ──────────────────┐ + * │ │ │ + * │ │ USK │ + * └──▶ USK ────────────┘ (not visible by │ + * Alice) │ + * ▼ + * ┌──────────────┐ + * │ BOB's Device │ + * └──────────────┘ + */ + + val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") + ?: return legacyFallbackTrust( + locallyTrusted, + DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey() + ?.unpaddedBase64PublicKey + ?: "" + ) + ) + + // Check bob's device is signed by bob's SSK + try { + olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + } catch (e: Throwable) { + return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e)) + } + + return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) + } + + private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { + return if (locallyTrusted == true) { + DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) + } else { + crossSignTrustFail + } + } + + override fun onUsersDeviceUpdate(users: List) { + Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users") + users.forEach { otherUserId -> + + checkUserTrust(otherUserId).let { + Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") + cryptoStore.setUserKeysAsTrusted(otherUserId, it.isVerified()) + } + + // TODO if my keys have changes, i should recheck all devices of all users? + val devices = cryptoStore.getUserDeviceList(otherUserId) + devices?.forEach { device -> + val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustLevel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustLevel.kt new file mode 100644 index 0000000000..4b08caf375 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustLevel.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.crosssigning + +data class DeviceTrustLevel(val crossSigningVerified: Boolean, val locallyVerified: Boolean?) { + + fun isVerified() = crossSigningVerified || locallyVerified == true + fun isCrossSigningVerified() = crossSigningVerified + fun isLocallyVerified() = locallyVerified +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustResult.kt new file mode 100644 index 0000000000..d1a1c83cd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustResult.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.crosssigning + +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo + +sealed class DeviceTrustResult { + + data class Success(val level: DeviceTrustLevel) : DeviceTrustResult() + data class UnknownDevice(val deviceID: String) : DeviceTrustResult() + data class CrossSigningNotConfigured(val userID: String) : DeviceTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : DeviceTrustResult() + data class MissingDeviceSignature(val deviceId: String, val signingKey: String) : DeviceTrustResult() + data class InvalidDeviceSignature(val deviceId: String, val signingKey: String, val throwable: Throwable?) : DeviceTrustResult() +} + +fun DeviceTrustResult.isSuccess(): Boolean = this is DeviceTrustResult.Success +fun DeviceTrustResult.isCrossSignedVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isCrossSigningVerified() == true +fun DeviceTrustResult.isLocallyVerified(): Boolean = (this as? DeviceTrustResult.Success)?.level?.isLocallyVerified() == true diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/Extensions.kt new file mode 100644 index 0000000000..6ffc341881 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/Extensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.crosssigning + +import android.util.Base64 +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.util.JsonCanonicalizer + +fun CryptoDeviceInfo.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun CryptoCrossSigningKey.canonicalSignable(): String { + return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) +} + +fun ByteArray.toBase64NoPadding(): String { + return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) +} + +fun String.fromBase64NoPadding(): ByteArray { + return Base64.decode(this, Base64.NO_PADDING or Base64.NO_WRAP) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/UserTrustResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/UserTrustResult.kt new file mode 100644 index 0000000000..d8c616cb64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/UserTrustResult.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.crosssigning + +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey + +sealed class UserTrustResult { + + object Success : UserTrustResult() + +// data class Success(val deviceID: String, val crossSigned: Boolean) : UserTrustResult() +// +// data class UnknownDevice(val deviceID: String) : UserTrustResult() + data class CrossSigningNotConfigured(val userID: String) : UserTrustResult() + data class UnknownCrossSignatureInfo(val userID: String) : UserTrustResult() + data class KeysNotTrusted(val key: MXCrossSigningInfo) : UserTrustResult() + data class KeyNotSigned(val key: CryptoCrossSigningKey) : UserTrustResult() + data class InvalidSignature(val key: CryptoCrossSigningKey, val signature: String) : UserTrustResult() +} + +fun UserTrustResult.isVerified() = this is UserTrustResult.Success diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt index 99267ee89c..73646de361 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -402,7 +402,7 @@ internal class KeysBackup @Inject constructor( } if (deviceId != null) { - val device = cryptoStore.getUserDevice(deviceId, userId) + val device = cryptoStore.getUserDevice(userId, deviceId) var isSignatureValid = false if (device == null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt index caf4aaad0b..e85e1cc433 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo /** * A signature in a the `KeyBackupVersionTrust` object. @@ -26,7 +26,7 @@ class KeyBackupVersionTrustSignature { /** * The device that signed the backup version. */ - var device: MXDeviceInfo? = null + var device: CryptoDeviceInfo? = null /** *Flag to indicate the signature from this device is valid. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt index d91189a4bf..b860afd36d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup.model -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo /** * A signature in a `KeysBackupVersionTrust` object. @@ -32,7 +32,7 @@ class KeysBackupVersionTrustSignature { * The device that signed the backup version. * Can be null if the device is not known. */ - var device: MXDeviceInfo? = null + var device: CryptoDeviceInfo? = null /** * Flag to indicate the signature from this device is valid. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoCrossSigningKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoCrossSigningKey.kt new file mode 100644 index 0000000000..bd438c54f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoCrossSigningKey.kt @@ -0,0 +1,92 @@ +package im.vector.matrix.android.internal.crypto.model + +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.rest.RestKeyInfo + +data class CryptoCrossSigningKey( + override val userId: String, + + val usages: List?, + + override val keys: Map, + + override val signatures: Map>?, + + var trustLevel: DeviceTrustLevel? = null +) : CryptoInfo { + + override fun signalableJSONDictionary(): Map { + val map = HashMap() + userId.let { map["user_id"] = it } + usages?.let { map["usage"] = it } + keys.let { map["keys"] = it } + + return map + } + + val unpaddedBase64PublicKey: String? = keys.values.firstOrNull() + + val isMasterKey = usages?.contains(KeyUsage.MASTER.value) ?: false + val isSelfSigningKey = usages?.contains(KeyUsage.SELF_SIGNING.value) ?: false + val isUserKey = usages?.contains(KeyUsage.USER_SIGNING.value) ?: false + + fun addSignatureAndCopy(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + val updated = (signatures?.toMutableMap() ?: HashMap()) + val userMap = updated[userId]?.toMutableMap() + ?: HashMap().also { updated[userId] = it } + userMap["ed25519:$signedWithNoPrefix"] = signature + + return this.copy( + signatures = updated + ) + } + + fun copyForSignature(userId: String, signedWithNoPrefix: String, signature: String): CryptoCrossSigningKey { + return this.copy( + signatures = mapOf(userId to mapOf("ed25519:$signedWithNoPrefix" to signature)) + ) + } + + data class Builder( + val userId: String, + val usage: KeyUsage, + private var base64Pkey: String? = null, + private val signatures: ArrayList> = ArrayList() + ) { + + fun key(publicKeyBase64: String) = apply { + base64Pkey = publicKeyBase64 + } + + fun signature(userId: String, keySignedBase64: String, base64Signature: String) = apply { + signatures.add(Triple(userId, keySignedBase64, base64Signature)) + } + + fun build(): CryptoCrossSigningKey { + val b64key = base64Pkey ?: throw IllegalArgumentException("") + + val signMap = HashMap>() + signatures.forEach { info -> + val uMap = signMap[info.first] + ?: HashMap().also { signMap[info.first] = it } + uMap["ed25519:${info.second}"] = info.third + } + + return CryptoCrossSigningKey( + userId = userId, + usages = listOf(usage.value), + keys = mapOf("ed25519:$b64key" to b64key), + signatures = signMap) + } + } +} + +enum class KeyUsage(val value: String) { + MASTER("master"), + SELF_SIGNING("self_signing"), + USER_SIGNING("user_signing") +} + +fun CryptoCrossSigningKey.toRest(): RestKeyInfo { + return CryptoInfoMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt new file mode 100644 index 0000000000..cede23f16a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.model + +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity + +data class CryptoDeviceInfo( + val deviceId: String, + override val userId: String, + var algorithms: List? = null, + override val keys: Map? = null, + override val signatures: Map>? = null, + val unsigned: JsonDict? = null, + +// TODO how to store if this device is verified by a user SSK, or is legacy trusted? +// I need to know if it is trusted via cross signing (Trusted because bob verified it) + + var trustLevel: DeviceTrustLevel? = null, + var isBlocked: Boolean = false + +) + + : CryptoInfo { + + val isVerified: Boolean + get() = trustLevel?.isVerified() ?: false + + val isUnknown: Boolean + get() = trustLevel == null + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("ed25519:$deviceId") + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return keys + ?.takeIf { !deviceId.isBlank() } + ?.get("curve25519:$deviceId") + } + + /** + * @return the display name + */ + fun displayName(): String? { + return unsigned?.get("device_display_name") as? String + } + + override fun signalableJSONDictionary(): Map { + val map = HashMap() + map["device_id"] = deviceId + map["user_id"] = userId + algorithms?.let { map["algorithms"] = it } + keys?.let { map["keys"] = it } + return map + } +// +// /** +// * @return a dictionary of the parameters +// */ +// fun toDeviceKeys(): DeviceKeys { +// return DeviceKeys( +// userId = userId, +// deviceId = deviceId, +// algorithms = algorithms!!, +// keys = keys!!, +// signatures = signatures!! +// ) +// } +} + +fun CryptoDeviceInfo.toRest(): RestDeviceInfo { + return CryptoInfoMapper.map(this) +} + +internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity { + return CryptoMapper.mapToEntity(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfo.kt new file mode 100644 index 0000000000..c9d49bede0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfo.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model + +/** + * Generic crypto info. + * Can be a device (CryptoDeviceInfo), as well as a CryptoCrossSigningInfo (can be seen as a kind of virtual device) + */ +interface CryptoInfo { + + val userId: String + + val keys: Map? + + val signatures: Map>? + + fun signalableJSONDictionary(): Map +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt new file mode 100644 index 0000000000..8d216f3160 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model + +import com.squareup.moshi.Moshi +import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.RestKeyInfo +import im.vector.matrix.android.internal.di.SerializeNulls + +object CryptoInfoMapper { + + private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + + fun map(restDeviceInfo: RestDeviceInfo): CryptoDeviceInfo { + return CryptoDeviceInfo( + deviceId = restDeviceInfo.deviceId, + userId = restDeviceInfo.userId, + algorithms = restDeviceInfo.algorithms, + keys = restDeviceInfo.keys, + signatures = restDeviceInfo.signatures, + unsigned = restDeviceInfo.unsigned, + trustLevel = null + ) + } + + fun map(cryptoDeviceInfo: CryptoDeviceInfo): RestDeviceInfo { + return RestDeviceInfo( + deviceId = cryptoDeviceInfo.deviceId, + algorithms = cryptoDeviceInfo.algorithms, + keys = cryptoDeviceInfo.keys, + signatures = cryptoDeviceInfo.signatures, + unsigned = cryptoDeviceInfo.unsigned, + userId = cryptoDeviceInfo.userId + ) + } + + fun map(keyInfo: RestKeyInfo): CryptoCrossSigningKey { + return CryptoCrossSigningKey( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys ?: emptyMap(), + signatures = keyInfo.signatures, + trustLevel = null + ) + } + + fun map(keyInfo: CryptoCrossSigningKey): RestKeyInfo { + return RestKeyInfo( + userId = keyInfo.userId, + usages = keyInfo.usages, + keys = keyInfo.keys, + signatures = keyInfo.signatures + ) + } + + fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo { + return map(this) + } + + fun CryptoDeviceInfo.toRest(): RestDeviceInfo { + return map(this) + } + +// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey { +// return map(this) +// } + + fun CryptoCrossSigningKey.toRest(): RestKeyInfo { + return map(this) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.kt index a92034873f..3f0dfe8b57 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.kt @@ -22,7 +22,7 @@ data class MXOlmSessionResult( /** * the device */ - val deviceInfo: MXDeviceInfo, + val deviceInfo: CryptoDeviceInfo, /** * Base64 olm session id. * null if no session could be established. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt index f823de2eb3..fc8eff2875 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class DeleteDeviceParams( @Json(name = "auth") - var deleteDeviceAuth: DeleteDeviceAuth? = null + var userPasswordAuth: UserPasswordAuth? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt index 3486bfdabe..27a3480d0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt @@ -18,22 +18,25 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) data class DeviceKeys( @Json(name = "user_id") - val userId: String, + val userId: String?, @Json(name = "device_id") - val deviceId: String, + val deviceId: String?, @Json(name = "algorithms") - val algorithms: List, + val algorithms: List?, @Json(name = "keys") - val keys: Map, + val keys: Map?, @Json(name = "signatures") - val signatures: JsonDict + val signatures: Map>?, + + @Json(name = "usage") + val usage: List? = null + ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt index bf1482ac9f..d012d03add 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -43,6 +43,8 @@ internal data class KeyVerificationKey( } } + override fun toSendToDeviceObject() = this + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { return false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt index 6bb1ae6644..d6e712e056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt @@ -27,7 +27,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMac internal data class KeyVerificationMac( @Json(name = "transaction_id") override val transactionID: String? = null, @Json(name = "mac") override val mac: Map? = null, - @Json(name = "key") override val keys: String? = null + @Json(name = "keys") override val keys: String? = null ) : SendToDeviceObject, VerificationInfoMac { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt index fa4ec3acfa..9e4b7b773e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction +import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber @@ -34,30 +34,48 @@ internal data class KeyVerificationStart( @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List? = null, @Json(name = "hashes") override val hashes: List? = null, @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List? = null, - @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null, + // For QR code verification + @Json(name = "secret") override val sharedSecret: String? = null ) : SendToDeviceObject, VerificationInfoStart { override fun toCanonicalJson(): String? { return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) } + // TODO Move those method to the interface? override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() - || method !in supportedVerificationMethods - || keyAgreementProtocols.isNullOrEmpty() - || hashes.isNullOrEmpty() - || !hashes.contains("sha256") - || messageAuthenticationCodes.isNullOrEmpty() - || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) - && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) - || shortAuthenticationStrings.isNullOrEmpty() - || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { + || (method == VERIFICATION_METHOD_SAS && !isValidSas()) + || (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) { Timber.e("## received invalid verification request") return false } return true } + private fun isValidSas(): Boolean { + if (keyAgreementProtocols.isNullOrEmpty() + || hashes.isNullOrEmpty() + || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() + || (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256) + && !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) + || shortAuthenticationStrings.isNullOrEmpty() + || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { + return false + } + + return true + } + + private fun isValidReciprocate(): Boolean { + if (sharedSecret.isNullOrBlank()) { + return false + } + + return true + } + override fun toSendToDeviceObject() = this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt index d13bf9954f..b2b62706a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt @@ -1,6 +1,5 @@ /* - * Copyright 2016 OpenMarket Ltd - * Copyright 2017 Vector Creations Ltd + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +18,14 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo /** * This class represents the response to /keys/query request made by downloadKeysForUsers + * + * After uploading cross-signing keys, they will be included under the /keys/query endpoint under the master_keys, + * self_signing_keys and user_signing_keys properties. + * + * The user_signing_keys property will only be included when a user requests their own keys. */ @JsonClass(generateAdapter = true) data class KeysQueryResponse( @@ -32,11 +35,21 @@ data class KeysQueryResponse( * TODO Use MXUsersDevicesMap? */ @Json(name = "device_keys") - var deviceKeys: Map>? = null, + var deviceKeys: Map>? = null, /** * The failures sorted by homeservers. TODO Bad comment ? * TODO Use MXUsersDevicesMap? */ - var failures: Map>? = null + var failures: Map>? = null, + + @Json(name = "master_keys") + var masterKeys: Map? = null, + + @Json(name = "self_signing_keys") + var selfSigningKeys: Map? = null, + + @Json(name = "user_signing_keys") + var userSigningKeys: Map? = null + ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt index f67cbd4766..961125767e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) data class KeysUploadBody( @Json(name = "device_keys") - var deviceKeys: DeviceKeys? = null, + var deviceKeys: RestDeviceInfo? = null, @Json(name = "one_time_keys") var oneTimeKeys: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestDeviceInfo.kt new file mode 100644 index 0000000000..e057780093 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestDeviceInfo.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class RestDeviceInfo( + /** + * The id of this device. + */ + @Json(name = "device_id") + var deviceId: String, + + /** + * the user id + */ + @Json(name = "user_id") + var userId: String, + + /** + * The list of algorithms supported by this device. + */ + @Json(name = "algorithms") + var algorithms: List? = null, + + /** + * A map from ":" to "". + */ + @Json(name = "keys") + var keys: Map? = null, + + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + @Json(name = "signatures") + var signatures: Map>? = null, + + /* + * Additional data from the home server. + */ + @Json(name = "unsigned") + var unsigned: JsonDict? = null) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestKeyInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestKeyInfo.kt new file mode 100644 index 0000000000..ffd1817402 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestKeyInfo.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.CryptoInfoMapper + +@JsonClass(generateAdapter = true) +data class RestKeyInfo( + /** + * The user who owns the key + */ + @Json(name = "user_id") + val userId: String, + /** + * Allowed uses for the key. + * Must contain "master" for master keys, "self_signing" for self-signing keys, and "user_signing" for user-signing keys. + * See CrossSigningKeyInfo#KEY_USAGE_* constants + */ + @Json(name = "usage") + val usages: List?, + + /** + * An object that must have one entry, + * whose name is "ed25519:" followed by the unpadded base64 encoding of the public key, + * and whose value is the unpadded base64 encoding of the public key. + */ + @Json(name = "keys") + val keys: Map?, + + /** + * Signatures of the key. + * A self-signing or user-signing key must be signed by the master key. + * A master key may be signed by a device. + */ + @Json(name = "signatures") + val signatures: Map>? = null +) { + fun toCryptoModel(): CryptoCrossSigningKey { + return CryptoInfoMapper.map(this) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SignatureUploadResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SignatureUploadResponse.kt new file mode 100644 index 0000000000..d8186ad2cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SignatureUploadResponse.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations 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.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Upload Signature response + */ +@JsonClass(generateAdapter = true) +data class SignatureUploadResponse( + + /** + * The response contains a failures property, which is a map of user ID to device ID to failure reason, + * if any of the uploaded keys failed. + * The homeserver should verify that the signatures on the uploaded keys are valid. + * If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object + * with the errcode property set to M_INVALID_SIGNATURE. + */ + var failures: Map>? = null + +) + +@JsonClass(generateAdapter = true) +data class UploadResponseFailure( + @Json(name = "status") + val status: Int, + @Json(name = "errCode") + val errCode: String, + @Json(name = "message") + val message: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt new file mode 100644 index 0000000000..f4e71c7dca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model.rest + +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.crypto.model.toRest + +/** + * Helper class to build CryptoApi#uploadSignatures params + */ +data class UploadSignatureQueryBuilder( + private val deviceInfoList: ArrayList = ArrayList(), + private val signingKeyInfoList: ArrayList = ArrayList() +) { + + fun withDeviceInfo(deviceInfo: CryptoDeviceInfo) = apply { + deviceInfoList.add(deviceInfo) + } + + fun withSigningKeyInfo(info: CryptoCrossSigningKey) = apply { + signingKeyInfoList.add(info) + } + + fun build(): Map> { + val map = HashMap>() + + val usersList = ( + deviceInfoList.map { it.userId } + + signingKeyInfoList + .map { it.userId } + ).distinct() + + usersList.forEach { userID -> + val userMap = HashMap() + deviceInfoList.filter { it.userId == userID }.forEach { deviceInfo -> + userMap[deviceInfo.deviceId] = deviceInfo.toRest() + } + signingKeyInfoList.filter { it.userId == userID }.forEach { keyInfo -> + keyInfo.unpaddedBase64PublicKey?.let { base64Key -> + userMap[base64Key] = keyInfo.toRest() + } + } + map[userID] = userMap + } + + return map + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSigningKeysBody.kt new file mode 100644 index 0000000000..2b485ea4b1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UploadSigningKeysBody( + @Json(name = "master_key") + val masterKey: RestKeyInfo? = null, + + @Json(name = "self_signing_key") + val selfSigningKey: RestKeyInfo? = null, + + @Json(name = "user_signing_key") + val userSigningKey: RestKeyInfo? = null, + + @Json(name = "auth") + val auth: UserPasswordAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt similarity index 86% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt index 53ba4179eb..0945de030a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt @@ -1,5 +1,6 @@ /* * Copyright 2016 OpenMarket Ltd + * Copyright 2020 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. @@ -17,12 +18,13 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes /** * This class provides the authentication data to delete a device */ @JsonClass(generateAdapter = true) -internal data class DeleteDeviceAuth( +data class UserPasswordAuth( // device device session id @Json(name = "session") @@ -30,7 +32,7 @@ internal data class DeleteDeviceAuth( // registration information @Json(name = "type") - var type: String? = null, + var type: String? = LoginFlowTypes.PASSWORD, @Json(name = "user") var user: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt index 168a8c8f48..643ac5a495 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt @@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.crypto.model.rest import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod internal const val VERIFICATION_METHOD_SAS = "m.sas.v1" -internal const val VERIFICATION_METHOD_SCAN = "m.qr_code.scan.v1" + +// Qr code +// Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#verification-methods +internal const val VERIFICATION_METHOD_QR_CODE_SHOW = "m.qr_code.show.v1" +internal const val VERIFICATION_METHOD_QR_CODE_SCAN = "m.qr_code.scan.v1" +internal const val VERIFICATION_METHOD_RECIPROCATE = "m.reciprocate.v1" internal fun VerificationMethod.toValue(): String { return when (this) { - VerificationMethod.SAS -> VERIFICATION_METHOD_SAS - VerificationMethod.SCAN -> VERIFICATION_METHOD_SCAN + VerificationMethod.SAS -> VERIFICATION_METHOD_SAS + VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW } } - -// TODO Add SCAN -internal val supportedVerificationMethods = listOf(VERIFICATION_METHOD_SAS) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 4e8dfcb2b0..a82ca8217d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -17,10 +17,14 @@ package im.vector.matrix.android.internal.crypto.store +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody @@ -154,7 +158,7 @@ internal interface IMXCryptoStore { * @param userId the user's id. * @param device the device to store. */ - fun storeUserDevice(userId: String?, deviceInfo: MXDeviceInfo?) + fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) /** * Retrieve a device for a user. @@ -163,7 +167,7 @@ internal interface IMXCryptoStore { * @param userId the user's id. * @return the device */ - fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? + fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? /** * Retrieve a device by its identity key. @@ -171,7 +175,7 @@ internal interface IMXCryptoStore { * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) * @return the device or null if not found */ - fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo? + fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? /** * Store the known devices for a user. @@ -179,7 +183,11 @@ internal interface IMXCryptoStore { * @param userId The user's id. * @param devices A map from device id to 'MXDevice' object for the device. */ - fun storeUserDevices(userId: String, devices: Map?) + fun storeUserDevices(userId: String, devices: Map?) + + fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) /** * Retrieve the known devices for a user. @@ -187,8 +195,11 @@ internal interface IMXCryptoStore { * @param userId The user's id. * @return The devices map if some devices are known, else null */ - fun getUserDevices(userId: String): Map? + fun getUserDevices(userId: String): Map? + fun getUserDeviceList(userId: String): List? + + fun getLiveDeviceList(userId: String): LiveData> /** * Store the crypto algorithm for a room. * @@ -381,4 +392,26 @@ internal interface IMXCryptoStore { fun addNewSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) + + // ============================================= + // CROSS SIGNING + // ============================================= + + /** + * Gets the current crosssigning info + */ + fun getMyCrossSigningInfo() : MXCrossSigningInfo? + fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) + + fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo? + fun getLiveCrossSigningInfo(userId: String) : LiveData> + fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) + + fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) + fun getCrossSigningPrivateKeys() : PrivateKeysInfo? + + fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean) + + fun clearOtherUserTrust() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt new file mode 100644 index 0000000000..a10b6d2645 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/PrivateKeysInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.store + +data class PrivateKeysInfo( + val master: String? = null, + val selfSigned: String? = null, + val user: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index c194a400ce..7f4d4c0c0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -16,22 +16,55 @@ package im.vector.matrix.android.internal.crypto.store.db +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.toEntity import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.store.db.model.* +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMapper +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey import im.vector.matrix.android.internal.crypto.store.db.query.delete +import im.vector.matrix.android.internal.crypto.store.db.query.get import im.vector.matrix.android.internal.crypto.store.db.query.getById import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import io.realm.RealmConfiguration +import io.realm.RealmList import io.realm.Sort import io.realm.kotlin.where import org.matrix.olm.OlmAccount @@ -69,6 +102,10 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati newSessionListeners.remove(listener) } + private val monarchy = Monarchy.Builder() + .setRealmConfiguration(realmConfiguration) + .build() + /* ========================================================================================== * Other data * ========================================================================================== */ @@ -166,20 +203,22 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati return olmAccount } - override fun storeUserDevice(userId: String?, deviceInfo: MXDeviceInfo?) { + override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) { if (userId == null || deviceInfo == null) { return } - doRealmTransaction(realmConfiguration) { - val user = UserEntity.getOrCreate(it, userId) + doRealmTransaction(realmConfiguration) { realm -> + val user = UserEntity.getOrCreate(realm, userId) // Create device info - val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply { - deviceId = deviceInfo.deviceId - identityKey = deviceInfo.identityKey() - putDeviceInfo(deviceInfo) - } + val deviceInfoEntity = CryptoMapper.mapToEntity(deviceInfo) + realm.insertOrUpdate(deviceInfoEntity) +// val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply { +// deviceId = deviceInfo.deviceId +// identityKey = deviceInfo.identityKey() +// putDeviceInfo(deviceInfo) +// } if (!user.devices.contains(deviceInfoEntity)) { user.devices.add(deviceInfoEntity) @@ -187,25 +226,28 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati } } - override fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? { + override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { return doRealmQueryAndCopy(realmConfiguration) { it.where() .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .findFirst() + }?.let { + CryptoMapper.mapToModel(it) } - ?.getDeviceInfo() } - override fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo? { + override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { return doRealmQueryAndCopy(realmConfiguration) { it.where() .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) .findFirst() } - ?.getDeviceInfo() + ?.let { + CryptoMapper.mapToModel(it) + } } - override fun storeUserDevices(userId: String, devices: Map?) { + override fun storeUserDevices(userId: String, devices: Map?) { doRealmTransaction(realmConfiguration) { realm -> if (devices == null) { // Remove the user @@ -216,32 +258,127 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati // Add the devices // Ensure all other devices are deleted u.devices.deleteAllFromRealm() - - u.devices.addAll( - devices.map { - DeviceInfoEntity.getOrCreate(realm, userId, it.value.deviceId).apply { - deviceId = it.value.deviceId - identityKey = it.value.identityKey() - putDeviceInfo(it.value) - } - } - ) + val new = devices.map { entry -> entry.value.toEntity() } + new.forEach { realm.insertOrUpdate(it) } + u.devices.addAll(new) } } } } - override fun getUserDevices(userId: String): Map? { + override fun storeUserCrossSigningKeys(userId: String, + masterKey: CryptoCrossSigningKey?, + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) { + doRealmTransaction(realmConfiguration) { realm -> + UserEntity.getOrCreate(realm, userId) + .let { userEntity -> + if (masterKey == null || selfSigningKey == null) { + // The user has disabled cross signing? + userEntity.crossSigningInfoEntity?.deleteFromRealm() + userEntity.crossSigningInfoEntity = null + } else { + CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> + // What should we do if we detect a change of the keys? + + val existingMaster = signingInfo.getMasterKey() + if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { + // update signatures? + existingMaster.putSignatures(masterKey.signatures) + existingMaster.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } else { + val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { + this.publicKeyBase64 = masterKey.unpaddedBase64PublicKey + this.usages = masterKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + this.putSignatures(masterKey.signatures) + } + signingInfo.setMasterKey(keyEntity) + } + + val existingSelfSigned = signingInfo.getSelfSignedKey() + if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { + // update signatures? + existingSelfSigned.putSignatures(selfSigningKey.signatures) + existingSelfSigned.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } else { + val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { + this.publicKeyBase64 = selfSigningKey.unpaddedBase64PublicKey + this.usages = selfSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + this.putSignatures(selfSigningKey.signatures) + } + signingInfo.setSelfSignedKey(keyEntity) + } + + userEntity.crossSigningInfoEntity = signingInfo + } + } + } + } + } + + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return doRealmQueryAndCopy(realmConfiguration) { realm -> + realm.where().findFirst() + }?.let { + PrivateKeysInfo( + master = it.xSignMasterPrivateKey, + selfSigned = it.xSignSelfSignedPrivateKey, + user = it.xSignUserPrivateKey + ) + } + } + + override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignMasterPrivateKey = msk + xSignSelfSignedPrivateKey = ssk + xSignUserPrivateKey = usk + } + } + } + + override fun getUserDevices(userId: String): Map? { return doRealmQueryAndCopy(realmConfiguration) { it.where() .equalTo(UserEntityFields.USER_ID, userId) .findFirst() } ?.devices - ?.mapNotNull { it.getDeviceInfo() } + ?.map { CryptoMapper.mapToModel(it) } ?.associateBy { it.deviceId } } + override fun getUserDeviceList(userId: String): List? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + } + ?.devices + ?.map { CryptoMapper.mapToModel(it) } + } + + override fun getLiveDeviceList(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: emptyList() + } + } + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm @@ -731,4 +868,173 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati } .toMutableList() } + + /* ========================================================================================== + * Cross Signing + * ========================================================================================== */ + override fun getMyCrossSigningInfo(): MXCrossSigningInfo? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + }?.userId?.let { + getCrossSigningInfo(it) + } + } + + override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.userId?.let { userId -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + } + + override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + xInfoEntity?.crossSigningKeys?.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = trusted + newLevel.crossSignedVerified = trusted + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = trusted + level.crossSignedVerified = trusted + } + } + } + } + + override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where(DeviceInfoEntity::class.java) + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst()?.let { deviceInfoEntity -> + val trustEntity = deviceInfoEntity.trustLevelEntity + if (trustEntity == null) { + realm.createObject(TrustLevelEntity::class.java).let { + it.locallyVerified = locallyVerified + it.crossSignedVerified = crossSignedVerified + deviceInfoEntity.trustLevelEntity = it + } + } else { + trustEntity.locallyVerified = locallyVerified + trustEntity.crossSignedVerified = crossSignedVerified + } + } + } + } + + override fun clearOtherUserTrust() { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { info -> + // Need to ignore mine + if (info.userId != credentials.userId) { + info.crossSigningKeys.forEach { + it.trustLevelEntity = null + } + } + } + } + } + + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { + return doRealmQueryAndCopy(realmConfiguration) { realm -> + realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + }?.let { xsignInfo -> + MXCrossSigningInfo( + userId = userId, + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + val pubKey = it.publicKeyBase64 ?: return@mapNotNull null + CryptoCrossSigningKey( + userId = userId, + keys = mapOf("ed25519:$pubKey" to pubKey), + usages = it.usages.map { it }, + signatures = it.getSignatures(), + trustLevel = it.trustLevelEntity?.let { + DeviceTrustLevel( + crossSigningVerified = it.crossSignedVerified ?: false, + locallyVerified = it.locallyVerified ?: false + ) + } + + ) + } + ) + } + } + + override fun getLiveCrossSigningInfo(userId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + }, + { entity -> + MXCrossSigningInfo( + userId = userId, + crossSigningKeys = entity.crossSigningKeys.mapNotNull { + val pubKey = it.publicKeyBase64 ?: return@mapNotNull null + CryptoCrossSigningKey( + userId = userId, + keys = mapOf("ed25519:$pubKey" to pubKey), + usages = it.usages.map { it }, + signatures = it.getSignatures(), + trustLevel = it.trustLevelEntity?.let { + DeviceTrustLevel( + crossSigningVerified = it.crossSignedVerified ?: false, + locallyVerified = it.locallyVerified ?: false + ) + } + ) + } + ) + } + ) + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + + override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { + doRealmTransaction(realmConfiguration) { realm -> + addOrUpdateCrossSigningInfo(realm, userId, info) + } + } + + private fun addOrUpdateCrossSigningInfo(realm: Realm, userId: String, info: MXCrossSigningInfo?): CrossSigningInfoEntity? { + var existing = CrossSigningInfoEntity.get(realm, userId) + if (info == null) { + // Delete known if needed + existing?.deleteFromRealm() + // TODO notify, we might need to untrust things? + } else { + // Just override existing, caller should check and untrust id needed + existing = CrossSigningInfoEntity.getOrCreate(realm, userId) + // existing.crossSigningKeys.forEach { it.deleteFromRealm() } + val xkeys = RealmList() + info.crossSigningKeys.forEach { cryptoCrossSigningKey -> + xkeys.add( + realm.createObject(KeyInfoEntity::class.java).also { keyInfoEntity -> + keyInfoEntity.publicKeyBase64 = cryptoCrossSigningKey.unpaddedBase64PublicKey + keyInfoEntity.usages = cryptoCrossSigningKey.usages?.let { RealmList(*it.toTypedArray()) } + ?: RealmList() + keyInfoEntity.putSignatures(cryptoCrossSigningKey.signatures) + // TODO how to handle better, check if same keys? + // reset trust + keyInfoEntity.trustLevelEntity = null + } + ) + } + existing.crossSigningKeys = xkeys + } + return existing + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 7c6fae88e8..6839f6995b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -16,15 +16,120 @@ package im.vector.matrix.android.internal.crypto.store.db +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import im.vector.matrix.android.internal.di.SerializeNulls import io.realm.DynamicRealm import io.realm.RealmMigration import timber.log.Timber internal object RealmCryptoStoreMigration : RealmMigration { - const val CRYPTO_STORE_SCHEMA_VERSION = 0L + // Version 1L added Cross Signing info persistence + const val CRYPTO_STORE_SCHEMA_VERSION = 1L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") + + if (oldVersion <= 0) { + Timber.d("Step 0 -> 1") + Timber.d("Create KeyInfoEntity") + + val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") + .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) + .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + + val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") + .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) + .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) + .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + + Timber.d("Create CrossSigningInfoEntity") + + val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") + .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) + .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) + .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) + + Timber.d("Updating UserEntity table") + realm.schema.get("UserEntity") + ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) + + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) + + val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) + ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) + ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + ?.transform { obj -> + + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm(oldSerializedData)?.let { oldDevice -> + + val trustLevel = realm.createObject("TrustLevelEntity") + when (oldDevice.verified) { + MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> { + obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`) + } + MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> { + trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED) + trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) + obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + } + + obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId) + obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey()) + obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms)) + obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys)) + obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) + obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) + } + } + ?.removeField("deviceInfoData") + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index 998274020a..9d7b823efb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -32,6 +32,9 @@ import io.realm.annotations.RealmModule OlmInboundGroupSessionEntity::class, OlmSessionEntity::class, OutgoingRoomKeyRequestEntity::class, - UserEntity::class + UserEntity::class, + KeyInfoEntity::class, + CrossSigningInfoEntity::class, + TrustLevelEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CrossSigningInfoEntity.kt new file mode 100644 index 0000000000..f07085003f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.store.db.model + +import im.vector.matrix.android.internal.crypto.model.KeyUsage +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CrossSigningInfoEntity( + @PrimaryKey + var userId: String? = null, + var crossSigningKeys: RealmList = RealmList() +) : RealmObject() { + + companion object + + fun getMasterKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.MASTER.value) } + + fun setMasterKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.MASTER.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } + + fun getSelfSignedKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + + fun setSelfSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.SELF_SIGNING.value) } + .forEach { crossSigningKeys.remove(it) } + info?.let { crossSigningKeys.add(it) } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt new file mode 100644 index 0000000000..5a4938d1fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.store.db.model + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo +import im.vector.matrix.android.internal.di.SerializeNulls +import timber.log.Timber + +object CryptoMapper { + + private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + private val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + private val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + private val mapOfStringMigrationAdapter = moshi.adapter>>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) + + internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity { + return DeviceInfoEntity( + primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId), + userId = deviceInfo.userId, + deviceId = deviceInfo.deviceId, + algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms), + keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys), + signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures), + isBlocked = deviceInfo.isBlocked, + trustLevelEntity = deviceInfo.trustLevel?.let { + TrustLevelEntity( + crossSignedVerified = it.crossSigningVerified, + locallyVerified = it.locallyVerified + ) + }, + unsignedMapJson = mapMigrationAdapter.toJson(deviceInfo.unsigned) + ) + } + + internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo { + return CryptoDeviceInfo( + userId = deviceInfoEntity.userId ?: "", + deviceId = deviceInfoEntity.deviceId ?: "", + isBlocked = deviceInfoEntity.isBlocked ?: false, + trustLevel = deviceInfoEntity.trustLevelEntity?.let { + DeviceTrustLevel(it.crossSignedVerified ?: false, it.locallyVerified) + }, + unsigned = deviceInfoEntity.unsignedMapJson?.let { + try { + mapMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + signatures = deviceInfoEntity.signatureMapJson?.let { + try { + mapOfStringMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + keys = deviceInfoEntity.keysMapJson?.let { + try { + moshi.adapter>(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )).fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + }, + algorithms = deviceInfoEntity.algorithmListJson?.let { + try { + listMigrationAdapter.fromJson(it) + } catch (failure: Throwable) { + Timber.e(failure) + null + } + } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt index eae202f966..8a2c2914da 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -34,7 +34,13 @@ internal open class CryptoMetadataEntity( // Settings for blacklisting unverified devices. var globalBlacklistUnverifiedDevices: Boolean = false, // The keys backup version currently used. Null means no backup. - var backupVersion: String? = null + var backupVersion: String? = null, + + var xSignMasterPrivateKey: String? = null, + var xSignUserPrivateKey: String? = null, + var xSignSelfSignedPrivateKey: String? = null + +// var crossSigningInfoEntity: CrossSigningInfoEntity? = null ) : RealmObject() { // Deserialize data diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt index 2c321cc50e..dceb77ddd9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -16,9 +16,6 @@ package im.vector.matrix.android.internal.crypto.store.db.model -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo -import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm -import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.LinkingObjects @@ -30,18 +27,26 @@ internal fun DeviceInfoEntity.Companion.createPrimaryKey(userId: String, deviceI internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "", var deviceId: String? = null, var identityKey: String? = null, - var deviceInfoData: String? = null) + // var deviceInfoData: String? = null, + var userId: String? = null, + var isBlocked: Boolean? = null, + var algorithmListJson: String? = null, + var keysMapJson: String? = null, + var signatureMapJson: String? = null, + var unsignedMapJson: String? = null, + var trustLevelEntity: TrustLevelEntity? = null + ) : RealmObject() { - // Deserialize data - fun getDeviceInfo(): MXDeviceInfo? { - return deserializeFromRealm(deviceInfoData) - } - - // Serialize data - fun putDeviceInfo(deviceInfo: MXDeviceInfo?) { - deviceInfoData = serializeForRealm(deviceInfo) - } +// // Deserialize data +// fun getDeviceInfo(): MXDeviceInfo? { +// return deserializeFromRealm(deviceInfoData) +// } +// +// // Serialize data +// fun putDeviceInfo(deviceInfo: MXDeviceInfo?) { +// deviceInfoData = serializeForRealm(deviceInfo) +// } @LinkingObjects("devices") val users: RealmResults? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt new file mode 100644 index 0000000000..c40c752fbe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeyInfoEntity.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.store.db.model + +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import io.realm.RealmList +import io.realm.RealmObject + +internal open class KeyInfoEntity( + var publicKeyBase64: String? = null, +// var isTrusted: Boolean = false, + var usages: RealmList = RealmList(), + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + var signatures: String? = null, + var trustLevelEntity: TrustLevelEntity? = null +) : RealmObject() { + + // Deserialize data + fun getSignatures(): Map>? { + return deserializeFromRealm(signatures) + } + + // Serialize data + fun putSignatures(deviceInfo: Map>?) { + signatures = serializeForRealm(deviceInfo) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/TrustLevelEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/TrustLevelEntity.kt new file mode 100644 index 0000000000..c35282b392 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/TrustLevelEntity.kt @@ -0,0 +1,13 @@ +package im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject + +internal open class TrustLevelEntity( + var crossSignedVerified: Boolean? = null, + var locallyVerified: Boolean? = null +) : RealmObject() { + + companion object + + fun isVerified() : Boolean = crossSignedVerified == true || locallyVerified == true +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt index 27cd7fe226..6b502c1403 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt @@ -20,9 +20,11 @@ import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey -internal open class UserEntity(@PrimaryKey var userId: String? = null, - var devices: RealmList = RealmList(), - var deviceTrackingStatus: Int = 0) +internal open class UserEntity( + @PrimaryKey var userId: String? = null, + var devices: RealmList = RealmList(), + var crossSigningInfoEntity: CrossSigningInfoEntity? = null, + var deviceTrackingStatus: Int = 0) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt new file mode 100644 index 0000000000..4c81cb46fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CrossSigningInfoEntityQueries.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.store.db.query + +import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun CrossSigningInfoEntity.Companion.getOrCreate(realm: Realm, userId: String): CrossSigningInfoEntity { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: realm.createObject(userId) +} + +internal fun CrossSigningInfoEntity.Companion.get(realm: Realm, userId: String): CrossSigningInfoEntity? { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index 19e0f6efb5..4ebdac2635 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.api.CryptoApi -import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest @@ -44,7 +44,7 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( return executeRequest(eventBus) { apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams() .apply { - deleteDeviceAuth = DeleteDeviceAuth() + userPasswordAuth = UserPasswordAuth() .apply { type = LoginFlowTypes.PASSWORD session = params.authSession diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index c285f16ee3..c9a8addd2a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -18,13 +18,13 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.MXCryptoError -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.VerificationService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task @@ -35,7 +35,7 @@ import javax.inject.Inject internal interface RoomVerificationUpdateTask : Task { data class Params( val events: List, - val sasVerificationService: DefaultSasVerificationService, + val sasVerificationService: DefaultVerificationService, val cryptoService: CryptoService ) } @@ -60,7 +60,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. - if (!SasVerificationService.isValidRequest(event.ageLocalTs + if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs)) return@forEach Unit.also { Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt index d8bfe73eda..4ea60a7bad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -18,9 +18,9 @@ package im.vector.matrix.android.internal.crypto.tasks import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.crypto.api.CryptoApi -import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse +import im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.convertToUTF8 @@ -30,7 +30,7 @@ import javax.inject.Inject internal interface UploadKeysTask : Task { data class Params( // the device keys to send. - val deviceKeys: DeviceKeys?, + val deviceKeys: RestDeviceInfo?, // the one-time keys to send. val oneTimeKeys: JsonDict?, // the explicit device_id to use for upload (default is to use the same as that used during auth). diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSignaturesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSignaturesTask.kt new file mode 100644 index 0000000000..fdaf14bfdb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSignaturesTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSignaturesTask : Task { + data class Params( + val signatures: Map> + ) +} + +internal class DefaultUploadSignaturesTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSignaturesTask { + + override suspend fun execute(params: UploadSignaturesTask.Params) { + try { + val response = executeRequest(eventBus) { + apiCall = cryptoApi.uploadSignatures(params.signatures) + } + if (response.failures?.isNotEmpty() == true) { + throw Throwable(response.failures.toString()) + } + return + } catch (f: Failure) { + throw f + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt new file mode 100644 index 0000000000..0a69039219 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2019 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.matrix.android.internal.crypto.tasks + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse +import im.vector.matrix.android.internal.crypto.model.rest.UploadSigningKeysBody +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.model.toRest +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface UploadSigningKeysTask : Task { + data class Params( + // the device keys to send. + val masterKey: CryptoCrossSigningKey, + // the one-time keys to send. + val userKey: CryptoCrossSigningKey, + // the explicit device_id to use for upload (default is to use the same as that used during auth). + val selfSignedKey: CryptoCrossSigningKey, + val userPasswordAuth: UserPasswordAuth? + ) +} + +data class UploadSigningKeys(val failures: Map?) : Failure.FeatureFailure() + +internal class DefaultUploadSigningKeysTask @Inject constructor( + private val cryptoApi: CryptoApi, + private val eventBus: EventBus +) : UploadSigningKeysTask { + override suspend fun execute(params: UploadSigningKeysTask.Params) { + val uploadQuery = UploadSigningKeysBody( + masterKey = params.masterKey.toRest(), + userSigningKey = params.userKey.toRest(), + selfSigningKey = params.selfSignedKey.toRest(), + auth = params.userPasswordAuth.takeIf { params.userPasswordAuth?.session != null } + ) + try { + // Make a first request to start user-interactive authentication + val request = executeRequest(eventBus) { + apiCall = cryptoApi.uploadSigningKeys(uploadQuery) + } + if (request.failures?.isNotEmpty() == true) { + throw UploadSigningKeys(request.failures) + } + return + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError + && throwable.httpCode == 401 + && params.userPasswordAuth != null + /* Avoid infinite loop */ + && params.userPasswordAuth.session.isNullOrEmpty() + ) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(throwable.errorBody) + } catch (e: Exception) { + null + }?.let { + // Retry with authentication + try { + val req = executeRequest(eventBus) { + apiCall = cryptoApi.uploadSigningKeys( + uploadQuery.copy(auth = params.userPasswordAuth.copy(session = it.session)) + ) + } + if (req.failures?.isNotEmpty() == true) { + throw UploadSigningKeys(req.failures) + } + return + } catch (failure: Throwable) { + throw failure + } + } + } + // Other error + throw throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt similarity index 71% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 349d6a79ca..13839a0499 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -17,65 +17,69 @@ package im.vector.matrix.android.internal.crypto.verification import android.util.Base64 import im.vector.matrix.android.BuildConfig -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber -internal class DefaultIncomingSASVerificationTransaction( +internal class DefaultIncomingSASDefaultVerificationTransaction( setDeviceVerificationAction: SetDeviceVerificationAction, - override val credentials: Credentials, + override val userId: String, + override val deviceId: String?, private val cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, deviceFingerprint: String, transactionId: String, otherUserID: String, - val autoAccept: Boolean = false -) : SASVerificationTransaction( + private val autoAccept: Boolean = false +) : SASDefaultVerificationTransaction( setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, + crossSigningService, deviceFingerprint, transactionId, otherUserID, null, - true), + isIncoming = true), IncomingSasVerificationTransaction { override val uxState: IncomingSasVerificationTransaction.UxState get() { return when (state) { - SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT - SasVerificationTxState.SendingAccept, - SasVerificationTxState.Accepted, - SasVerificationTxState.OnKeyReceived, - SasVerificationTxState.SendingKey, - SasVerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT - SasVerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS - SasVerificationTxState.ShortCodeAccepted, - SasVerificationTxState.SendingMac, - SasVerificationTxState.MacSent, - SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION - SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED - SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME - SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER - else -> IncomingSasVerificationTransaction.UxState.UNKNOWN + VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT + VerificationTxState.SendingAccept, + VerificationTxState.Accepted, + VerificationTxState.OnKeyReceived, + VerificationTxState.SendingKey, + VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS + VerificationTxState.ShortCodeAccepted, + VerificationTxState.SendingMac, + VerificationTxState.MacSent, + VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED + VerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME + VerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + else -> IncomingSasVerificationTransaction.UxState.UNKNOWN } } override fun onVerificationStart(startReq: VerificationInfoStart) { Timber.v("## SAS I: received verification request from state $state") - if (state != SasVerificationTxState.None) { + if (state != VerificationTxState.None) { Timber.e("## SAS I: received verification request from invalid state") // should I cancel?? throw IllegalStateException("Interactive Key verification already started") } this.startReq = startReq - state = SasVerificationTxState.OnStarted + state = VerificationTxState.OnStarted this.otherDeviceId = startReq.fromDevice if (autoAccept) { @@ -84,7 +88,7 @@ internal class DefaultIncomingSASVerificationTransaction( } override fun performAccept() { - if (state != SasVerificationTxState.OnStarted) { + if (state != VerificationTxState.OnStarted) { Timber.e("## SAS Cannot perform accept from state $state") return } @@ -108,7 +112,7 @@ internal class DefaultIncomingSASVerificationTransaction( } // Bob’s device ensures that it has a copy of Alice’s device key. - val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId) + val mxDeviceInfo = cryptoStore.getUserDevice(userId = otherUserId, deviceId = otherDeviceId!!) if (mxDeviceInfo?.fingerprint() == null) { Timber.e("## SAS Failed to find device key ") @@ -140,11 +144,11 @@ internal class DefaultIncomingSASVerificationTransaction( val concat = getSAS().publicKey + startReq!!.toCanonicalJson() accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" // we need to send this to other device now - state = SasVerificationTxState.SendingAccept - sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, SasVerificationTxState.Accepted, CancelCode.User) { - if (state == SasVerificationTxState.SendingAccept) { + state = VerificationTxState.SendingAccept + sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, VerificationTxState.Accepted, CancelCode.User) { + if (state == VerificationTxState.SendingAccept) { // It is possible that we receive the next event before this one :/, in this case we should keep state - state = SasVerificationTxState.Accepted + state = VerificationTxState.Accepted } } } @@ -154,9 +158,9 @@ internal class DefaultIncomingSASVerificationTransaction( cancel(CancelCode.UnexpectedMessage) } - override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) { + override fun onKeyVerificationKey(vKey: VerificationInfoKey) { Timber.v("## SAS received key for request id:$transactionId") - if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { + if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { Timber.e("## SAS received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return @@ -170,11 +174,11 @@ internal class DefaultIncomingSASVerificationTransaction( val keyToDevice = transport.createKey(transactionId, pubKey) // we need to send this to other device now - state = SasVerificationTxState.SendingKey - this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { - if (state == SasVerificationTxState.SendingKey) { + state = VerificationTxState.SendingKey + this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { + if (state == VerificationTxState.SendingKey) { // It is possible that we receive the next event before this one :/, in this case we should keep state - state = SasVerificationTxState.KeySent + state = VerificationTxState.KeySent } } @@ -191,10 +195,7 @@ internal class DefaultIncomingSASVerificationTransaction( // - the Matrix ID of the user who sent the m.key.verification.accept message, // - he device ID of the device that sent the m.key.verification.accept message // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + - "$otherUserId$otherDeviceId" + - "${credentials.userId}${credentials.deviceId}" + - transactionId + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId" // decimal: generate five bytes by using HKDF. // emoji: generate six bytes by using HKDF. shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) @@ -204,18 +205,18 @@ internal class DefaultIncomingSASVerificationTransaction( Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") } - state = SasVerificationTxState.ShortCodeReady + state = VerificationTxState.ShortCodeReady } override fun onKeyVerificationMac(vKey: VerificationInfoMac) { Timber.v("## SAS I: received mac for request id:$transactionId") // Check for state? - if (state != SasVerificationTxState.SendingKey - && state != SasVerificationTxState.KeySent - && state != SasVerificationTxState.ShortCodeReady - && state != SasVerificationTxState.ShortCodeAccepted - && state != SasVerificationTxState.SendingMac - && state != SasVerificationTxState.MacSent) { + if (state != VerificationTxState.SendingKey + && state != VerificationTxState.KeySent + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { Timber.e("## SAS I: received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt similarity index 68% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 6e6f55b9fb..3e693f53ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -15,55 +15,57 @@ */ package im.vector.matrix.android.internal.crypto.verification -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.rest.toValue import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber -internal class DefaultOutgoingSASVerificationRequest( +internal class DefaultOutgoingSASDefaultVerificationTransaction( setDeviceVerificationAction: SetDeviceVerificationAction, - credentials: Credentials, + userId: String, + deviceId: String?, cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, deviceFingerprint: String, transactionId: String, otherUserId: String, otherDeviceId: String -) : SASVerificationTransaction( +) : SASDefaultVerificationTransaction( setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, + crossSigningService, deviceFingerprint, transactionId, otherUserId, otherDeviceId, isIncoming = false), - OutgoingSasVerificationRequest { + OutgoingSasVerificationTransaction { - override val uxState: OutgoingSasVerificationRequest.UxState + override val uxState: OutgoingSasVerificationTransaction.UxState get() { return when (state) { - SasVerificationTxState.None -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_START - SasVerificationTxState.SendingStart, - SasVerificationTxState.Started, - SasVerificationTxState.OnAccepted, - SasVerificationTxState.SendingKey, - SasVerificationTxState.KeySent, - SasVerificationTxState.OnKeyReceived -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT - SasVerificationTxState.ShortCodeReady -> OutgoingSasVerificationRequest.UxState.SHOW_SAS - SasVerificationTxState.ShortCodeAccepted, - SasVerificationTxState.SendingMac, - SasVerificationTxState.MacSent, - SasVerificationTxState.Verifying -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION - SasVerificationTxState.Verified -> OutgoingSasVerificationRequest.UxState.VERIFIED - SasVerificationTxState.OnCancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME - SasVerificationTxState.Cancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER - else -> OutgoingSasVerificationRequest.UxState.UNKNOWN + VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START + VerificationTxState.SendingStart, + VerificationTxState.Started, + VerificationTxState.OnAccepted, + VerificationTxState.SendingKey, + VerificationTxState.KeySent, + VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS + VerificationTxState.ShortCodeAccepted, + VerificationTxState.SendingMac, + VerificationTxState.MacSent, + VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED + VerificationTxState.OnCancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME + VerificationTxState.Cancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN } } @@ -72,16 +74,15 @@ internal class DefaultOutgoingSASVerificationRequest( cancel(CancelCode.UnexpectedMessage) } - fun start(method: VerificationMethod) { - if (state != SasVerificationTxState.None) { + fun start() { + if (state != VerificationTxState.None) { Timber.e("## SAS O: start verification from invalid state") // should I cancel?? throw IllegalStateException("Interactive Key verification already started") } - val startMessage = transport.createStart( - credentials.deviceId ?: "", - method.toValue(), + val startMessage = transport.createStartForSas( + deviceId ?: "", transactionId, KNOWN_AGREEMENT_PROTOCOLS, KNOWN_HASHES, @@ -90,19 +91,19 @@ internal class DefaultOutgoingSASVerificationRequest( ) startReq = startMessage - state = SasVerificationTxState.SendingStart + state = VerificationTxState.SendingStart sendToOther( EventType.KEY_VERIFICATION_START, startMessage, - SasVerificationTxState.Started, + VerificationTxState.Started, CancelCode.User, null ) } // fun request() { -// if (state != SasVerificationTxState.None) { +// if (state != VerificationTxState.None) { // Timber.e("## start verification from invalid state") // // should I cancel?? // throw IllegalStateException("Interactive Key verification already started") @@ -118,7 +119,7 @@ internal class DefaultOutgoingSASVerificationRequest( // sendToOther( // EventType.KEY_VERIFICATION_REQUEST, // requestMessage, -// SasVerificationTxState.None, +// VerificationTxState.None, // CancelCode.User, // null // ) @@ -126,7 +127,7 @@ internal class DefaultOutgoingSASVerificationRequest( override fun onVerificationAccept(accept: VerificationInfoAccept) { Timber.v("## SAS O: onVerificationAccept id:$transactionId") - if (state != SasVerificationTxState.Started) { + if (state != VerificationTxState.Started) { Timber.e("## SAS O: received accept request from invalid state $state") cancel(CancelCode.UnexpectedMessage) return @@ -144,7 +145,7 @@ internal class DefaultOutgoingSASVerificationRequest( // Upon receipt of the m.key.verification.accept message from Bob’s device, // Alice’s device stores the commitment value for later use. accepted = accept - state = SasVerificationTxState.OnAccepted + state = VerificationTxState.OnAccepted // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA @@ -152,18 +153,18 @@ internal class DefaultOutgoingSASVerificationRequest( val keyToDevice = transport.createKey(transactionId, pubKey) // we need to send this to other device now - state = SasVerificationTxState.SendingKey - sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { + state = VerificationTxState.SendingKey + sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, VerificationTxState.KeySent, CancelCode.User) { // It is possible that we receive the next event before this one :/, in this case we should keep state - if (state == SasVerificationTxState.SendingKey) { - state = SasVerificationTxState.KeySent + if (state == VerificationTxState.SendingKey) { + state = VerificationTxState.KeySent } } } - override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) { + override fun onKeyVerificationKey(vKey: VerificationInfoKey) { Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") - if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { + if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { Timber.e("## received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return @@ -189,27 +190,24 @@ internal class DefaultOutgoingSASVerificationRequest( // - the Matrix ID of the user who sent the m.key.verification.accept message, // - he device ID of the device that sent the m.key.verification.accept message // - the transaction ID. - val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + - "${credentials.userId}${credentials.deviceId}" + - "$otherUserId$otherDeviceId" + - transactionId + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId" // decimal: generate five bytes by using HKDF. // emoji: generate six bytes by using HKDF. shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) - state = SasVerificationTxState.ShortCodeReady + state = VerificationTxState.ShortCodeReady } else { - // bad commitement + // bad commitment cancel(CancelCode.MismatchedCommitment) } } override fun onKeyVerificationMac(vKey: VerificationInfoMac) { Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") - if (state != SasVerificationTxState.OnKeyReceived - && state != SasVerificationTxState.ShortCodeReady - && state != SasVerificationTxState.ShortCodeAccepted - && state != SasVerificationTxState.SendingMac - && state != SasVerificationTxState.MacSent) { + if (state != VerificationTxState.OnKeyReceived + && state != VerificationTxState.ShortCodeReady + && state != VerificationTxState.ShortCodeAccepted + && state != VerificationTxState.SendingMac + && state != VerificationTxState.MacSent) { Timber.e("## SAS O: received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt similarity index 61% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 8fa91e2f4a..9d7e531750 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -20,43 +20,76 @@ import android.os.Handler import android.os.Looper import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.crypto.sas.* +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod +import im.vector.matrix.android.api.session.crypto.sas.VerificationService +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS +import im.vector.matrix.android.internal.crypto.model.rest.toValue import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction +import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData +import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret +import im.vector.matrix.android.internal.di.DeviceId +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import okhttp3.internal.toImmutableList import timber.log.Timber -import java.util.* +import java.util.UUID import javax.inject.Inject -import kotlin.collections.ArrayList -import kotlin.collections.HashMap import kotlin.collections.set @SessionScope -internal class DefaultSasVerificationService @Inject constructor( - private val credentials: Credentials, +internal class DefaultVerificationService @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory, - private val sasTransportToDeviceFactory: SasTransportToDeviceFactory -) : VerificationTransaction.Listener, SasVerificationService { + private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, + private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, + private val crossSigningService: CrossSigningService +) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -64,7 +97,7 @@ internal class DefaultSasVerificationService @Inject constructor( lateinit var cryptoService: CryptoService // map [sender : [transaction]] - private val txMap = HashMap>() + private val txMap = HashMap>() /** * Map [sender: [PendingVerificationRequest]] @@ -135,9 +168,9 @@ internal class DefaultSasVerificationService @Inject constructor( } } - private var listeners = ArrayList() + private var listeners = ArrayList() - override fun addListener(listener: SasVerificationService.SasVerificationListener) { + override fun addListener(listener: VerificationService.VerificationListener) { uiHandler.post { if (!listeners.contains(listener)) { listeners.add(listener) @@ -145,7 +178,7 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun removeListener(listener: SasVerificationService.SasVerificationListener) { + override fun removeListener(listener: VerificationService.VerificationListener) { uiHandler.post { listeners.remove(listener) } @@ -200,9 +233,9 @@ internal class DefaultSasVerificationService @Inject constructor( } override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, - deviceID, - userId) + setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + userId, + deviceID) listeners.forEach { try { @@ -232,7 +265,7 @@ internal class DefaultSasVerificationService @Inject constructor( ?: return val senderId = event.senderId ?: return - if (requestInfo.toUserId != credentials.userId) { + if (requestInfo.toUserId != userId) { // I should ignore this, it's not for me Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me") return @@ -285,7 +318,7 @@ internal class DefaultSasVerificationService @Inject constructor( if (startReq?.isValid()?.not() == true) { Timber.e("## received invalid verification request") if (startReq.transactionID != null) { - sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) .cancelTransaction( startReq.transactionID ?: "", otherUserId!!, @@ -297,9 +330,9 @@ internal class DefaultSasVerificationService @Inject constructor( } handleStart(otherUserId, startReq as VerificationInfoStart) { - it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) + it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) }?.let { - sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) .cancelTransaction( startReq.transactionID ?: "", otherUserId!!, @@ -318,7 +351,7 @@ internal class DefaultSasVerificationService @Inject constructor( if (!startReq.isValid()) { Timber.e("## SAS received invalid verification request") if (startReq.transactionID != null) { - sasTransportToDeviceFactory.createTransport(null).cancelTransaction( + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( startReq.transactionID, otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -329,9 +362,9 @@ internal class DefaultSasVerificationService @Inject constructor( } // Download device keys prior to everything handleStart(otherUserId, startReq) { - it.transport = sasTransportToDeviceFactory.createTransport(it) + it.transport = verificationTransportToDeviceFactory.createTransport(it) }?.let { - sasTransportToDeviceFactory.createTransport(null).cancelTransaction( + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -340,70 +373,98 @@ internal class DefaultSasVerificationService @Inject constructor( } } - private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { - Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") + /** + * Return a CancelCode to make the caller cancel the verification. Else return null + */ + private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { + Timber.d("## SAS onStartRequestReceived ${startReq.transactionID}") if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) { - Timber.v("## SAS onStartRequestReceived $startReq") val tid = startReq.transactionID!! val existing = getExistingTransaction(otherUserId, tid) - val existingTxs = getExistingTransactionsForUser(otherUserId) - if (existing != null) { - // should cancel both! - Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") - existing.cancel(CancelCode.UnexpectedMessage) - return CancelCode.UnexpectedMessage - // cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } else if (existingTxs?.isEmpty() == false) { - Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") - // Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. - existingTxs.forEach { - it.cancel(CancelCode.UnexpectedMessage) - } - return CancelCode.UnexpectedMessage - // cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } else { - // Ok we can create - if (startReq.method == VERIFICATION_METHOD_SAS) { + + when (startReq.method) { + VERIFICATION_METHOD_SAS -> { + when (existing) { + is SasVerificationTransaction -> { + // should cancel both! + Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionID}") + existing.cancel(CancelCode.UnexpectedMessage) + // Already cancelled, so return null + return null + } + is QrCodeVerificationTransaction -> { + // Nothing to do? + } + null -> { + getExistingTransactionsForUser(otherUserId) + ?.filterIsInstance(SasVerificationTransaction::class.java) + ?.takeIf { it.isNotEmpty() } + ?.also { + // Multiple keyshares between two devices: + // any two devices may only have at most one key verification in flight at a time. + Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionID}") + } + ?.forEach { + it.cancel(CancelCode.UnexpectedMessage) + } + ?.also { + return CancelCode.UnexpectedMessage + } + } + } + + // Ok we can create a SAS transaction Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") // If there is a corresponding request, we can auto accept // as we are the one requesting in first place (or we accepted the request) val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID } ?: false - val tx = DefaultIncomingSASVerificationTransaction( + val tx = DefaultIncomingSASDefaultVerificationTransaction( // this, setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, + crossSigningService, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionID!!, otherUserId, autoAccept).also { txConfigure(it) } addTransaction(tx) tx.acceptVerificationEvent(otherUserId, startReq) - } else { + return null + } + VERIFICATION_METHOD_RECIPROCATE -> { + // Other user has scanned my QR code + if (existing is DefaultQrCodeVerificationTransaction) { + existing.onStartReceived(startReq) + return null + } else { + Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionID}") + return CancelCode.UnexpectedMessage + } + } + else -> { Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") return CancelCode.UnknownMethod - // cancelTransaction(tid, otherUserId, startReq.fromDevice -// ?: event.getSenderKey()!!, CancelCode.UnknownMethod) } } } else { return CancelCode.UnexpectedMessage -// cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) } - return null } + // TODO Refacto: It could just return a boolean private suspend fun checkKeysAreDownloaded(otherUserId: String, - fromDevice: String): MXUsersDevicesMap? { + otherDeviceId: String): MXUsersDevicesMap? { return try { var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) - if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) { + if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) { return keys } else { // force download keys = deviceListManager.downloadKeys(listOf(otherUserId), true) - return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true } + return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true } } } catch (e: Exception) { null @@ -456,9 +517,9 @@ internal class DefaultSasVerificationService @Inject constructor( )) } - if (existingTransaction is SASVerificationTransaction) { + if (existingTransaction is SASDefaultVerificationTransaction) { existingTransaction.cancelledReason = safeValueOf(cancelReq.code) - existingTransaction.state = SasVerificationTxState.OnCancelled + existingTransaction.state = VerificationTxState.OnCancelled } } @@ -492,7 +553,7 @@ internal class DefaultSasVerificationService @Inject constructor( return } - if (existing is SASVerificationTransaction) { + if (existing is SASDefaultVerificationTransaction) { existing.acceptVerificationEvent(otherUserId, acceptReq) } else { // not other types now @@ -533,7 +594,7 @@ internal class DefaultSasVerificationService @Inject constructor( Timber.e("## SAS Received invalid key request") return } - if (existing is SASVerificationTransaction) { + if (existing is SASDefaultVerificationTransaction) { existing.acceptVerificationEvent(otherUserId, keyReq) } else { // not other types now @@ -568,12 +629,12 @@ internal class DefaultSasVerificationService @Inject constructor( return } if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { - Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown") + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") // TODO cancel? return } - handleReadyReceived(event.senderId, readyReq) + handleReadyReceived(event.senderId, event.roomId!!, readyReq) } private fun onRoomDoneReceived(event: Event) { @@ -611,20 +672,94 @@ internal class DefaultSasVerificationService @Inject constructor( Timber.e("## SAS Received invalid Mac request") return } - if (existing is SASVerificationTransaction) { + if (existing is SASDefaultVerificationTransaction) { existing.acceptVerificationEvent(senderId, macReq) } else { // not other types known for now } } - private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) { + private fun handleReadyReceived(senderId: String, roomId: String, readyReq: VerificationInfoReady) { val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID } if (existingRequest == null) { Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}") return } - updatePendingRequest(existingRequest.copy(readyInfo = readyReq)) + + val qrCodeData = readyReq.methods + // Check if other user is able to scan QR code + ?.takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } + ?.let { + createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId) + } + + if (readyReq.methods?.orEmpty().orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE)) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction, + readyReq.transactionID!!, + senderId, + readyReq.fromDevice, + crossSigningService, + cryptoStore, + qrCodeData, + userId, + deviceId ?: "", + false) + + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + + addTransaction(tx) + } + + updatePendingRequest(existingRequest.copy( + readyInfo = readyReq + )) + } + + private fun createQrCodeData(transactionId: String?, otherUserId: String): QrCodeData? { + // Build the QR code URL + val requestEventId = transactionId ?: run { + Timber.w("## Unknown requestEventId") + return null + } + + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId) + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get other user master key") + return null + } + + val myDeviceId = deviceId + ?: run { + Timber.w("## Unable to get my deviceId") + return null + } + + val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()!! + + val generatedSharedSecret = generateSharedSecret() + return QrCodeData( + userId = userId, + requestEventId = requestEventId, + action = QrCodeData.ACTION_VERIFY, + keys = hashMapOf( + myMasterKey to myMasterKey, + myDeviceId to myDeviceKey + ), + sharedSecret = generatedSharedSecret, + otherUserKey = otherUserMasterKey + ) } private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) { @@ -636,21 +771,22 @@ internal class DefaultSasVerificationService @Inject constructor( updatePendingRequest(existingRequest.copy(isSuccessful = true)) } - override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? { + // TODO All this methods should be delegated to a TransactionStore + override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { synchronized(lock = txMap) { - return txMap[otherUser]?.get(tid) + return txMap[otherUserId]?.get(tid) } } - override fun getExistingVerificationRequest(otherUser: String): List? { + override fun getExistingVerificationRequest(otherUserId: String): List? { synchronized(lock = pendingRequests) { - return pendingRequests[otherUser] + return pendingRequests[otherUserId] } } - override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? { + override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { synchronized(lock = pendingRequests) { - return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } } + return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } } } } @@ -676,7 +812,7 @@ internal class DefaultSasVerificationService @Inject constructor( } } - private fun addTransaction(tx: VerificationTransaction) { + private fun addTransaction(tx: DefaultVerificationTransaction) { tx.otherUserId.let { otherUserId -> synchronized(txMap) { val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } @@ -687,60 +823,79 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String? { - val txID = createUniqueIDForTransaction(userId, deviceID) + override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String? { + val txID = createUniqueIDForTransaction(otherUserId, otherDeviceID) // should check if already one (and cancel it) if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASVerificationRequest( + val tx = DefaultOutgoingSASDefaultVerificationTransaction( setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, + crossSigningService, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, - userId, - deviceID) - tx.transport = sasTransportToDeviceFactory.createTransport(tx) + otherUserId, + otherDeviceID) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) addTransaction(tx) - tx.start(method) + tx.start() return txID } else { throw IllegalArgumentException("Unknown verification method") } } - override fun requestKeyVerificationInDMs(methods: List, userId: String, roomId: String) + override fun requestKeyVerificationInDMs(methods: List, otherUserId: String, roomId: String, localId: String?) : PendingVerificationRequest { - Timber.i("## SAS Requesting verification to user: $userId in room $roomId") + Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") - val requestsForUser = pendingRequests[userId] + val requestsForUser = pendingRequests[otherUserId] ?: ArrayList().also { - pendingRequests[userId] = it + pendingRequests[otherUserId] = it } - val transport = sasTransportRoomMessageFactory.createTransport(roomId, null) + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) // Cancel existing pending requests? - requestsForUser.forEach { existingRequest -> + requestsForUser.toImmutableList().forEach { existingRequest -> existingRequest.transactionId?.let { tid -> if (!existingRequest.isFinished) { Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) } } } - val localID = LocalEcho.createLocalEchoId() + val localID = localId ?: LocalEcho.createLocalEchoId() val verificationRequest = PendingVerificationRequest( ageLocalTs = System.currentTimeMillis(), isIncoming = false, roomId = roomId, localID = localID, - otherUserId = userId + otherUserId = otherUserId ) - transport.sendVerificationRequest(methods.map { it.toValue() }, localID, userId, roomId) { syncedId, info -> + // We can SCAN or SHOW QR codes only if cross-signing is enabled + val methodValues = if (crossSigningService.isCrossSigningEnabled()) { + // Add reciprocate method if application declares it can scan or show QR codes + // Not sure if it ok to do that (?) + val reciprocateMethod = methods + .firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW } + ?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty() + methods.map { it.toValue() } + reciprocateMethod + } else { + // Filter out SCAN and SHOW qr code method + methods + .filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN } + .map { it.toValue() } + } + .distinct() + + transport.sendVerificationRequest(methodValues, localID, otherUserId, roomId) { syncedId, info -> // We need to update with the syncedID updatePendingRequest(verificationRequest.copy( transactionId = syncedId, @@ -755,7 +910,7 @@ internal class DefaultSasVerificationService @Inject constructor( } override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) { - sasTransportRoomMessageFactory.createTransport(roomId, null) + verificationTransportRoomMessageFactory.createTransport(roomId, null) .cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User) getExistingVerificationRequest(otherUserId, transactionId)?.let { @@ -788,41 +943,53 @@ internal class DefaultSasVerificationService @Inject constructor( otherDeviceId: String, callback: MatrixCallback?): String? { if (method == VerificationMethod.SAS) { - val tx = DefaultOutgoingSASVerificationRequest( + val tx = DefaultOutgoingSASDefaultVerificationTransaction( setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, + crossSigningService, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, otherDeviceId) - tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, tx) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) addTransaction(tx) - tx.start(method) + tx.start() return transactionId } else { throw IllegalArgumentException("Unknown verification method") } } - override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean { + override fun readyPendingVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + transactionId: String): Boolean { Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") // Let's find the related request val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) if (existingRequest != null) { // we need to send a ready event, with matching methods - val transport = sasTransportRoomMessageFactory.createTransport(roomId, null) - val methods = existingRequest.requestInfo?.methods?.intersect(supportedVerificationMethods)?.toList() + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + roomId, + existingRequest.requestInfo?.methods, + methods) if (methods.isNullOrEmpty()) { Timber.i("Cannot ready this request, no common methods found txId:$transactionId") // TODO buttons should not be shown in this case? return false } // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? - val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods) - transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg, - SasVerificationTxState.None, + val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, + readyMsg, + VerificationTxState.None, CancelCode.User, null // TODO handle error? ) @@ -835,25 +1002,91 @@ internal class DefaultSasVerificationService @Inject constructor( } } + private fun computeReadyMethods( + transactionId: String, + otherUserId: String, + otherDeviceId: String, + roomId: String, + otherUserMethods: List?, + methods: List): List { + if (otherUserMethods.isNullOrEmpty()) { + return emptyList() + } + + val result = mutableSetOf() + + if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) { + // Other can do SAS and so do I + result.add(VERIFICATION_METHOD_SAS) + } + + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) { + // Other user wants to verify using QR code. Cross-signing has to be setup + val qrCodeData = createQrCodeData(transactionId, otherUserId) + + if (qrCodeData != null) { + if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) { + // Other can Scan and I can show QR code + result.add(VERIFICATION_METHOD_QR_CODE_SHOW) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) { + // Other can show and I can scan QR code + result.add(VERIFICATION_METHOD_QR_CODE_SCAN) + result.add(VERIFICATION_METHOD_RECIPROCATE) + } + } + + if (VERIFICATION_METHOD_RECIPROCATE in result) { + // Create the pending transaction + val tx = DefaultQrCodeVerificationTransaction( + setDeviceVerificationAction, + transactionId, + otherUserId, + otherDeviceId, + crossSigningService, + cryptoStore, + qrCodeData, + userId, + deviceId ?: "", + false) + + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + + addTransaction(tx) + } + } + + return result.toList() + } + /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ - private fun createUniqueIDForTransaction(userId: String, deviceID: String): String { + private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String { return buildString { - append(credentials.userId).append("|") - append(credentials.deviceId).append("|") append(userId).append("|") - append(deviceID).append("|") + append(deviceId).append("|") + append(otherUserId).append("|") + append(otherDeviceID).append("|") append(UUID.randomUUID().toString()) } } override fun transactionUpdated(tx: VerificationTransaction) { dispatchTxUpdated(tx) - if (tx is SASVerificationTransaction - && (tx.state == SasVerificationTxState.Cancelled - || tx.state == SasVerificationTxState.OnCancelled - || tx.state == SasVerificationTxState.Verified) + if (tx is SASDefaultVerificationTransaction + && (tx.state == VerificationTxState.Cancelled + || tx.state == VerificationTxState.OnCancelled + || tx.state == VerificationTxState.Verified) + ) { + // remove + this.removeTransaction(tx.otherUserId, tx.transactionId) + } + if (tx is QrCodeVerificationTransaction + && (tx.state == VerificationTxState.Cancelled + || tx.state == VerificationTxState.OnCancelled + || tx.state == VerificationTxState.Verified) ) { // remove this.removeTransaction(tx.otherUserId, tx.transactionId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt similarity index 80% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index d6cc5e3279..6f06b93b88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -15,17 +15,18 @@ */ package im.vector.matrix.android.internal.crypto.verification -import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction /** * Generic interactive key verification transaction */ -internal abstract class VerificationTransaction( +internal abstract class DefaultVerificationTransaction( override val transactionId: String, override val otherUserId: String, override var otherDeviceId: String? = null, - override val isIncoming: Boolean) : SasVerificationTransaction { + override val isIncoming: Boolean) : VerificationTransaction { + + lateinit var transport: VerificationTransport interface Listener { fun transactionUpdated(tx: VerificationTransaction) @@ -42,6 +43,4 @@ internal abstract class VerificationTransaction( } abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo) - - abstract fun cancel(code: CancelCode) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 0ac3847a53..cc0df09adb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -18,12 +18,14 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SCAN -import java.util.* +import java.util.UUID /** * Stores current pending verification requests + * TODO We should not expose this whole object to the app. Create an interface */ data class PendingVerificationRequest( val ageLocalTs: Long, @@ -37,7 +39,6 @@ data class PendingVerificationRequest( val cancelConclusion: CancelCode? = null, val isSuccessful: Boolean = false, val handledByOtherSession: Boolean = false - ) { val isReady: Boolean = readyInfo != null val isSent: Boolean = transactionId != null @@ -46,8 +47,9 @@ data class PendingVerificationRequest( fun hasMethod(method: VerificationMethod): Boolean? { return when (method) { - VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS) - VerificationMethod.SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SCAN) + VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS) + VerificationMethod.QR_CODE_SHOW -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW) + VerificationMethod.QR_CODE_SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt similarity index 70% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 4a12b05b49..495326d59d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -16,17 +16,20 @@ package im.vector.matrix.android.internal.crypto.verification import android.os.Build -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation import im.vector.matrix.android.api.session.crypto.sas.SasMode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.extensions.toUnsignedInt +import im.vector.matrix.android.internal.util.withoutPrefix import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber @@ -35,18 +38,18 @@ import kotlin.properties.Delegates /** * Represents an ongoing short code interactive key verification between two devices. */ -internal abstract class SASVerificationTransaction( +internal abstract class SASDefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, - open val credentials: Credentials, + open val userId: String, + open val deviceId: String?, private val cryptoStore: IMXCryptoStore, + private val crossSigningService: CrossSigningService, private val deviceFingerprint: String, transactionId: String, otherUserId: String, - otherDevice: String?, - isIncoming: Boolean) : - VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) { - - lateinit var transport: SasTransport + otherDeviceId: String?, + isIncoming: Boolean +) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction { companion object { const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" @@ -68,7 +71,7 @@ internal abstract class SASVerificationTransaction( } } - override var state by Delegates.observable(SasVerificationTxState.None) { _, _, new -> + override var state by Delegates.observable(VerificationTxState.None) { _, _, new -> // println("$property has changed from $old to $new") listeners.forEach { try { @@ -77,9 +80,9 @@ internal abstract class SASVerificationTransaction( Timber.e(e, "## Error while notifying listeners") } } - if (new == SasVerificationTxState.Cancelled - || new == SasVerificationTxState.OnCancelled - || new == SasVerificationTxState.Verified) { + if (new == VerificationTxState.Cancelled + || new == VerificationTxState.OnCancelled + || new == VerificationTxState.Verified) { releaseSAS() } } @@ -118,14 +121,14 @@ internal abstract class SASVerificationTransaction( */ override fun userHasVerifiedShortCode() { Timber.v("## SAS short code verified by user for id:$transactionId") - if (state != SasVerificationTxState.ShortCodeReady) { + if (state != VerificationTxState.ShortCodeReady) { // ignore and cancel? Timber.e("## Accepted short code from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } - state = SasVerificationTxState.ShortCodeAccepted + state = VerificationTxState.ShortCodeAccepted // Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated, // sorted list of the key IDs that they wish the other user to verify, // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: @@ -136,15 +139,37 @@ internal abstract class SASVerificationTransaction( // - the device ID of the device receiving the MAC, // - the transaction ID, and // - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs. + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId" - val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + - credentials.userId + credentials.deviceId + - otherUserId + otherDeviceId + - transactionId + // Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key. + // It should now contain both the device key and the MSK. + // So when Alice and Bob verify with SAS, the verification will verify the MSK. - val keyId = "ed25519:${credentials.deviceId}" + val keyMap = HashMap() + + val keyId = "ed25519:$deviceId" val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId) - val keyStrings = macUsingAgreedMethod(keyId, baseInfo + "KEY_IDS") + + if (macString.isNullOrBlank()) { + // Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + keyMap[keyId] = macString + + cryptoStore.getMyCrossSigningInfo()?.takeIf { it.isTrusted() } + ?.masterKey() + ?.unpaddedBase64PublicKey + ?.let { masterPublicKey -> + val crossSigningKeyId = "ed25519:$masterPublicKey" + macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString -> + keyMap[crossSigningKeyId] = MSKMacString + } + } + + val keyStrings = macUsingAgreedMethod(keyMap.keys.sorted().joinToString(","), baseInfo + "KEY_IDS") if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) { // Should not happen @@ -153,13 +178,13 @@ internal abstract class SASVerificationTransaction( return } - val macMsg = transport.createMac(transactionId, mapOf(keyId to macString), keyStrings) + val macMsg = transport.createMac(transactionId, keyMap, keyStrings) myMac = macMsg - state = SasVerificationTxState.SendingMac - sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) { - if (state == SasVerificationTxState.SendingMac) { + state = VerificationTxState.SendingMac + sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) { + if (state == VerificationTxState.SendingMac) { // It is possible that we receive the next event before this one :/, in this case we should keep state - state = SasVerificationTxState.MacSent + state = VerificationTxState.MacSent } } @@ -174,11 +199,15 @@ internal abstract class SASVerificationTransaction( cancel(CancelCode.MismatchedSas) } + override fun isToDeviceTransport(): Boolean { + return transport is VerificationTransportToDevice + } + override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { when (info) { is VerificationInfoStart -> onVerificationStart(info) is VerificationInfoAccept -> onVerificationAccept(info) - is VerificationInfoKey -> onKeyVerificationKey(senderId, info) + is VerificationInfoKey -> onKeyVerificationKey(info) is VerificationInfoMac -> onKeyVerificationMac(info) else -> { // nop @@ -190,13 +219,13 @@ internal abstract class SASVerificationTransaction( abstract fun onVerificationAccept(accept: VerificationInfoAccept) - abstract fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) + abstract fun onKeyVerificationKey(vKey: VerificationInfoKey) abstract fun onKeyVerificationMac(vKey: VerificationInfoMac) protected fun verifyMacs() { Timber.v("## SAS verifying macs for id:$transactionId") - state = SasVerificationTxState.Verifying + state = VerificationTxState.Verifying // Keys have been downloaded earlier in process val otherUserKnownDevices = cryptoStore.getUserDevices(otherUserId) @@ -208,7 +237,7 @@ internal abstract class SASVerificationTransaction( val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + otherUserId + otherDeviceId + - credentials.userId + credentials.deviceId + + userId + deviceId + transactionId val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",") @@ -224,10 +253,10 @@ internal abstract class SASVerificationTransaction( // cannot be empty because it has been validated theirMac!!.mac!!.keys.forEach { - val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it + val keyIDNoPrefix = it.withoutPrefix("ed25519:") val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() if (otherDeviceKey == null) { - Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify") + Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify") // just ignore and continue return@forEach } @@ -241,26 +270,70 @@ internal abstract class SASVerificationTransaction( verifiedDevices.add(keyIDNoPrefix) } + var otherMasterKeyIsVerified = false + val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() + val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey + if (otherCrossSigningMasterKeyPublic != null) { + // Did the user signed his master key + theirMac!!.mac!!.keys.forEach { + val keyIDNoPrefix = it.withoutPrefix("ed25519:") + if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { + // Check the signature + val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) + if (mac != theirMac?.mac?.get(it)) { + // WRONG! + Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") + cancel(CancelCode.MismatchedKeys) + return + } else { + otherMasterKeyIsVerified = true + } + } + } + } + // if none of the keys could be verified, then error because the app // should be informed about that - if (verifiedDevices.isEmpty()) { + if (verifiedDevices.isEmpty() && !otherMasterKeyIsVerified) { Timber.e("## SAS Verification: No devices verified") cancel(CancelCode.MismatchedKeys) return } + // If not me sign his MSK and upload the signature + if (otherMasterKeyIsVerified && otherUserId != userId) { + // we should trust this master key + // And check verification MSK -> SSK? + crossSigningService.trustUser(otherUserId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS Verification: Failed to trust User $otherUserId") + } + }) + } + + if (otherUserId == userId) { + // If me it's reasonable to sign and upload the device signature + // Notice that i might not have the private keys, so may not be able to do it + crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId") + } + }) + } + // TODO what if the otherDevice is not in this list? and should we verifiedDevices.forEach { - setDeviceVerified(it, otherUserId) + setDeviceVerified(otherUserId, it) } transport.done(transactionId) - state = SasVerificationTxState.Verified + state = VerificationTxState.Verified } - private fun setDeviceVerified(deviceId: String, userId: String) { - setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, - deviceId, - userId) + private fun setDeviceVerified(userId: String, deviceId: String) { + // TODO should not override cross sign status + setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + userId, + deviceId) } override fun cancel() { @@ -269,13 +342,13 @@ internal abstract class SASVerificationTransaction( override fun cancel(code: CancelCode) { cancelledReason = code - state = SasVerificationTxState.Cancelled + state = VerificationTxState.Cancelled transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) } protected fun sendToOther(type: String, keyToDevice: VerificationInfo, - nextState: SasVerificationTxState, + nextState: VerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt index d0cfe06815..4875705580 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoStart : VerificationInfo { val method: String? + /** * Alice’s device ID */ @@ -58,5 +59,10 @@ internal interface VerificationInfoStart : VerificationInfo { */ val shortAuthenticationStrings: List? + /** + * Shared secret, when starting verification with QR code + */ + val sharedSecret: String? + fun toCanonicalJson(): String? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt index ede5c42ad6..c6a6581384 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -37,7 +37,7 @@ internal class VerificationMessageLiveObserver @Inject constructor( @SessionDatabase realmConfiguration: RealmConfiguration, private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, private val cryptoService: CryptoService, - private val sasVerificationService: DefaultSasVerificationService, + private val sasVerificationService: DefaultVerificationService, private val taskExecutor: TaskExecutor ) : RealmLiveEntityObserver(realmConfiguration) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt similarity index 65% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index 12fe5c338f..286b099168 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -16,29 +16,29 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent /** - * SAS verification can be performed using toDevice events or via DM. - * This class abstracts the concept of transport for SAS + * Verification can be performed using toDevice events or via DM. + * This class abstracts the concept of transport for verification */ -internal interface SasTransport { +internal interface VerificationTransport { /** * Sends a message */ fun sendToOther(type: String, verificationInfo: VerificationInfo, - nextState: SasVerificationTxState, + nextState: VerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) fun sendVerificationRequest(supportedMethods: List, localID: String, otherUserId: String, - roomId: String, callback: - (String?, MessageVerificationRequestContent?) -> Unit) + roomId: String, + callback: (String?, MessageVerificationRequestContent?) -> Unit) fun cancelTransaction(transactionId: String, otherUserId: String, @@ -46,6 +46,7 @@ internal interface SasTransport { code: CancelCode) fun done(transactionId: String) + /** * Creates an accept message suitable for this transport */ @@ -59,13 +60,22 @@ internal interface SasTransport { fun createKey(tid: String, pubKey: String): VerificationInfoKey - fun createStart(fromDevice: String, - method: String, - transactionID: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List): VerificationInfoStart + /** + * Create start for SAS verification + */ + fun createStartForSas(fromDevice: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart + + /** + * Create start for QR code verification + */ + fun createStartForQrCode(fromDevice: String, + transactionID: String, + sharedSecret: String): VerificationInfoStart fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt similarity index 78% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index c9799cc58d..e20b4ae131 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -16,14 +16,33 @@ package im.vector.matrix.android.internal.crypto.verification import androidx.lifecycle.Observer -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.Operation +import androidx.work.WorkInfo import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState -import im.vector.matrix.android.api.session.events.model.* -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.LocalEcho +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserId @@ -35,11 +54,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber -import java.util.* +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject -internal class SasTransportRoomMessage( +internal class VerificationTransportRoomMessage( private val workManagerProvider: WorkManagerProvider, private val stringProvider: StringProvider, private val sessionId: String, @@ -48,12 +67,12 @@ internal class SasTransportRoomMessage( private val roomId: String, private val monarchy: Monarchy, private val localEchoEventFactory: LocalEchoEventFactory, - private val tx: SASVerificationTransaction? -) : SasTransport { + private val tx: DefaultVerificationTransaction? +) : VerificationTransport { override fun sendToOther(type: String, verificationInfo: VerificationInfo, - nextState: SasVerificationTxState, + nextState: VerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") @@ -245,24 +264,42 @@ internal class SasTransportRoomMessage( override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) - override fun createStart(fromDevice: String, - method: String, - transactionID: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List): VerificationInfoStart { + override fun createStartForSas(fromDevice: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { return MessageVerificationStartContent( fromDevice, hashes, keyAgreementProtocols, messageAuthenticationCodes, shortAuthenticationStrings, - method, + VERIFICATION_METHOD_SAS, RelationDefaultContent( type = RelationType.REFERENCE, eventId = transactionID - ) + ), + null + ) + } + + override fun createStartForQrCode(fromDevice: String, + transactionID: String, + sharedSecret: String): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + null, + null, + null, + null, + VERIFICATION_METHOD_RECIPROCATE, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionID + ), + sharedSecret ) } @@ -292,7 +329,7 @@ internal class SasTransportRoomMessage( } } -internal class SasTransportRoomMessageFactory @Inject constructor( +internal class VerificationTransportRoomMessageFactory @Inject constructor( private val workManagerProvider: WorkManagerProvider, private val stringProvider: StringProvider, private val monarchy: Monarchy, @@ -304,7 +341,7 @@ internal class SasTransportRoomMessageFactory @Inject constructor( private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory) { - fun createTransport(roomId: String, tx: SASVerificationTransaction?): SasTransportRoomMessage { - return SasTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, monarchy, localEchoEventFactory, tx) + fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { + return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, monarchy, localEchoEventFactory, tx) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt similarity index 73% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index 8d280f50d8..59ab9bf1cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -17,22 +17,29 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber import javax.inject.Inject -internal class SasTransportToDevice( - private var tx: SASVerificationTransaction?, +internal class VerificationTransportToDevice( + private var tx: DefaultVerificationTransaction?, private var sendToDeviceTask: SendToDeviceTask, private var taskExecutor: TaskExecutor -) : SasTransport { +) : VerificationTransport { override fun sendVerificationRequest(supportedMethods: List, localID: String, @@ -44,7 +51,7 @@ internal class SasTransportToDevice( override fun sendToOther(type: String, verificationInfo: VerificationInfo, - nextState: SasVerificationTxState, + nextState: VerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") @@ -119,21 +126,35 @@ internal class SasTransportToDevice( override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) - override fun createStart(fromDevice: String, - method: String, - transactionID: String, - keyAgreementProtocols: List, - hashes: List, - messageAuthenticationCodes: List, - shortAuthenticationStrings: List): VerificationInfoStart { + override fun createStartForSas(fromDevice: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { return KeyVerificationStart( fromDevice, - method, + VERIFICATION_METHOD_SAS, transactionID, keyAgreementProtocols, hashes, messageAuthenticationCodes, - shortAuthenticationStrings) + shortAuthenticationStrings, + null) + } + + override fun createStartForQrCode(fromDevice: String, + transactionID: String, + sharedSecret: String): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_RECIPROCATE, + transactionID, + null, + null, + null, + null, + sharedSecret) } override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { @@ -145,11 +166,11 @@ internal class SasTransportToDevice( } } -internal class SasTransportToDeviceFactory @Inject constructor( +internal class VerificationTransportToDeviceFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, private val taskExecutor: TaskExecutor) { - fun createTransport(tx: SASVerificationTransaction?): SasTransportToDevice { - return SasTransportToDevice(tx, sendToDeviceTask, taskExecutor) + fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { + return VerificationTransportToDevice(tx, sendToDeviceTask, taskExecutor) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt new file mode 100644 index 0000000000..9dafd4ff51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction +import im.vector.matrix.android.internal.crypto.verification.VerificationInfo +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart +import im.vector.matrix.android.internal.util.withoutPrefix +import timber.log.Timber +import kotlin.properties.Delegates + +internal class DefaultQrCodeVerificationTransaction( + private val setDeviceVerificationAction: SetDeviceVerificationAction, + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String?, + private val crossSigningService: CrossSigningService, + private val cryptoStore: IMXCryptoStore, + // Not null only if other user is able to scan QR code + private val qrCodeData: QrCodeData?, + val userId: String, + val deviceId: String, + override val isIncoming: Boolean +) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction { + + override var cancelledReason: CancelCode? = null + + override val qrCodeText: String? + get() = qrCodeData?.toUrl() + + override var state by Delegates.observable(VerificationTxState.None) { _, _, _ -> + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + override fun userHasScannedOtherQrCode(otherQrCodeText: String) { + val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run { + Timber.d("## Verification QR: Invalid QR Code Data") + cancel(CancelCode.QrCodeInvalid) + return + } + + // Perform some checks + if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) { + Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}") + cancel(CancelCode.QrCodeInvalid) + return + } + + if (otherQrCodeData.userId != otherUserId) { + Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}") + cancel(CancelCode.MismatchedUser) + return + } + + if (otherQrCodeData.requestEventId != transactionId) { + Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestEventId} expected:$transactionId") + cancel(CancelCode.QrCodeInvalid) + return + } + + // check master key + if (otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}") + cancel(CancelCode.MismatchedKeys) + return + } + + val toVerifyDeviceIds = mutableListOf() + var canTrustOtherUserMasterKey = false + + val otherDevices = cryptoStore.getUserDevices(otherUserId) + otherQrCodeData.keys.keys.forEach { key -> + Timber.w("## Verification QR: Checking key $key") + + when (val keyNoPrefix = key.withoutPrefix("ed25519:")) { + otherQrCodeData.keys[key] -> { + // Maybe master key? + if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { + canTrustOtherUserMasterKey = true + } else { + cancel(CancelCode.MismatchedKeys) + return + } + } + else -> { + when (val otherDevice = otherDevices?.get(keyNoPrefix)) { + null -> { + // Unknown device, ignore + } + else -> { + when (otherDevice.fingerprint()) { + null -> { + // Ignore + } + otherQrCodeData.keys[key] -> { + // Store the deviceId to verify after + toVerifyDeviceIds.add(key) + } + else -> { + cancel(CancelCode.MismatchedKeys) + return + } + } + } + } + } + } + } + + if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { + // Nothing to verify + cancel(CancelCode.MismatchedKeys) + return + } + + // All checks are correct + // Send the shared secret so that sender can trust me + // qrCodeData.sharedSecret will be used to send the start request + start(otherQrCodeData.sharedSecret) + + // Trust the other user + trust(canTrustOtherUserMasterKey, toVerifyDeviceIds) + } + + fun start(remoteSecret: String) { + if (state != VerificationTxState.None) { + Timber.e("## Verification QR: start verification from invalid state") + // should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + val startMessage = transport.createStartForQrCode( + deviceId, + transactionId, + remoteSecret + ) + + transport.sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.Started, + CancelCode.User, + null + ) + } + + override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + cancelledReason = code + state = VerificationTxState.Cancelled + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) + } + + override fun isToDeviceTransport() = false + + // Other user has scanned our QR code. check that the secret matched, so we can trust him + fun onStartReceived(startReq: VerificationInfoStart) { + if (qrCodeData == null) { + // Should not happen + cancel(CancelCode.UnexpectedMessage) + return + } + + if (startReq.sharedSecret == qrCodeData.sharedSecret) { + // Ok, we can trust the other user + // We can only trust the master key in this case + trust(true, emptyList()) + } else { + // Display a warning + cancel(CancelCode.MismatchedKeys) + } + } + + private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List) { + // If not me sign his MSK and upload the signature + if (otherUserId != userId && canTrustOtherUserMasterKey) { + // we should trust this master key + // And check verification MSK -> SSK? + crossSigningService.trustUser(otherUserId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId") + } + }) + } + + if (otherUserId == userId) { + // If me it's reasonable to sign and upload the device signature + // Notice that i might not have the private keys, so may not be able to do it + crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId") + } + }) + } + + // TODO what if the otherDevice is not in this list? and should we + toVerifyDeviceIds.forEach { + setDeviceVerified(otherUserId, it) + } + transport.done(transactionId) + state = VerificationTxState.Verified + } + + private fun setDeviceVerified(userId: String, deviceId: String) { + // TODO should not override cross sign status + setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + userId, + deviceId) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt new file mode 100644 index 0000000000..36b1a7b86b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.permalinks.PermalinkFactory +import java.net.URLDecoder +import java.net.URLEncoder + +private const val ENCODING = "utf-8" + +/** + * Generate an URL to generate a QR code of the form: + *
+ * https://matrix.to/#/?
+ *     request=
+ *     &action=verify
+ *     &key_=...
+ *     &secret=
+ *     &other_user_key=
+ *
+ * Example:
+ * https://matrix.to/#/@user:matrix.org?
+ *     request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
+ *     &action=verify
+ *     &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
+ *     &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
+ *     &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
+ *     &other_user_key=WqSVLkBCS%2Fi5NqR%2F%2FymC8T7K9RPxBIuqK8Usl6Y3big
+ * 
+ */ +fun QrCodeData.toUrl(): String { + return buildString { + append(PermalinkFactory.createPermalink(userId)) + append("?request=") + append(URLEncoder.encode(requestEventId, ENCODING)) + append("&action=") + append(URLEncoder.encode(action, ENCODING)) + + for ((keyId, key) in keys) { + append("&key_$keyId=") + append(URLEncoder.encode(key, ENCODING)) + } + + append("&secret=") + append(URLEncoder.encode(sharedSecret, ENCODING)) + append("&other_user_key=") + append(URLEncoder.encode(otherUserKey, ENCODING)) + } +} + +fun String.toQrCodeData(): QrCodeData? { + if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) { + return null + } + + val fragment = substringAfter("#") + if (fragment.isEmpty()) { + return null + } + + val safeFragment = fragment.substringBefore("?") + + // we are limiting to 2 params + val params = safeFragment + .split(MatrixPatterns.SEP_REGEX.toRegex()) + .filter { it.isNotEmpty() } + + if (params.size != 1) { + return null + } + + val userId = params.getOrNull(0) + ?.let { PermalinkFactory.unescape(it) } + ?.takeIf { MatrixPatterns.isUserId(it) } ?: return null + + val urlParams = fragment.substringAfter("?") + .split("&".toRegex()) + .filter { it.isNotEmpty() } + + val keyValues = urlParams.map { + (it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) }) + }.toMap() + + val action = keyValues["action"] ?: return null + + val requestEventId = keyValues["request"]?.takeIf { MatrixPatterns.isEventId(it) } ?: return null + val sharedSecret = keyValues["secret"] ?: return null + val otherUserKey = keyValues["other_user_key"] ?: return null + + val keys = keyValues.keys + .filter { it.startsWith("key_") } + .map { + it.substringAfter("key_") to (keyValues[it] ?: return null) + } + .toMap() + + return QrCodeData( + userId, + requestEventId, + action, + keys, + sharedSecret, + otherUserKey + ) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt new file mode 100644 index 0000000000..8b400413b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format + */ +data class QrCodeData( + val userId: String, + // the event ID of the associated verification request event. + val requestEventId: String, + // The action + val action: String, + // key_: each key that the user wants verified will have an entry of this form, where the value is the key in unpadded base64. + // The QR code should contain at least the user's master cross-signing key. + val keys: Map, + // random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded). + val sharedSecret: String, + // the other user's master cross-signing key, in unpadded base64. In other words, if Alice is displaying the QR code, + // this would be the copy of Bob's master cross-signing key that Alice has. + val otherUserKey: String +) { + companion object { + const val ACTION_VERIFY = "verify" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt new file mode 100644 index 0000000000..d319ebd88c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import java.security.SecureRandom + +fun generateSharedSecret(): String { + val secureRandom = SecureRandom() + + // 256 bits long + val secretBytes = ByteArray(32) + secureRandom.nextBytes(secretBytes) + return secretBytes.toBase64NoPadding() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 61e13483ab..409c844f0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams @@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask @@ -76,15 +78,21 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val roomId = RoomSummaryEntity.where(realm) + val candidates = RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - .findAll()?.let { dms -> - dms.firstOrNull { - it.otherMemberIds.contains(otherUserId) - } + .findAll()?.filter { dm -> + dm.otherMemberIds.contains(otherUserId) + && dm.membership == Membership.JOIN + }?.map { + it.roomId } - ?.roomId ?: return null - return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) } + ?: return null + candidates.forEach { roomId -> + if (RoomMemberHelper(realm, roomId).getActiveRoomMemberIds().any { it == otherUserId }) { + return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) } + } + } + return null } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 6f1593bc08..201e0b2322 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event @@ -75,6 +76,9 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) localMutableContent.remove(it) } + crypto.downloadKeys(listOf("@testxsigningvfe:matrix.org"), true, object : MatrixCallback { + }) + var error: Throwable? = null var result: MXEncryptEventContentResult? = null try { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt index 91397fae7e..285673a1f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult -import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.ToDeviceSyncResponse @@ -32,7 +32,7 @@ import timber.log.Timber import javax.inject.Inject internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, - private val sasVerificationService: DefaultSasVerificationService) { + private val sasVerificationService: DefaultVerificationService) { fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) { val total = toDevice.events?.size ?: 0 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt index f19bebe482..b3f240f23d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -49,3 +49,5 @@ fun convertFromUTF8(s: String): String { s } } + +fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this diff --git a/matrix-sdk-android/src/main/res/xml/network_security_config.xml b/matrix-sdk-android/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..e40c61c229 --- /dev/null +++ b/matrix-sdk-android/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ + + + + + + + + + + localhost + 127.0.0.1 + + 10.0.2.2 + + + diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt new file mode 100644 index 0000000000..3c184907a6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2020 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.matrix.android.internal.crypto.verification.qrcode + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldNotBeNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@Suppress("SpellCheckingInspection") +@FixMethodOrder(MethodSorters.JVM) +class QrCodeTest { + + private val basicQrCodeData = QrCodeData( + userId = "@benoit:matrix.org", + requestEventId = "\$azertyazerty", + action = QrCodeData.ACTION_VERIFY, + keys = mapOf( + "1" to "abcdef", + "2" to "ghijql" + ), + sharedSecret = "sharedSecret", + otherUserKey = "otherUserKey" + ) + + private val basicUrl = "https://matrix.to/#/@benoit:matrix.org?request=%24azertyazerty&action=verify&key_1=abcdef&key_2=ghijql&secret=sharedSecret&other_user_key=otherUserKey" + + @Test + fun testNominalCase() { + val url = basicQrCodeData.toUrl() + + url shouldBeEqualTo basicUrl + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit:matrix.org" + decodedData.requestEventId shouldBeEqualTo "\$azertyazerty" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey shouldBeEqualTo "otherUserKey" + } + + @Test + fun testSlashCase() { + val url = basicQrCodeData + .copy( + userId = "@benoit/foo:matrix.org", + requestEventId = "\$azertyazerty/bar" + ) + .toUrl() + + url shouldBeEqualTo basicUrl + .replace("@benoit", "@benoit%2Ffoo") + .replace("azertyazerty", "azertyazerty%2Fbar") + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit/foo:matrix.org" + decodedData.requestEventId shouldBeEqualTo "\$azertyazerty/bar" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey shouldBeEqualTo "otherUserKey" + } + + @Test + fun testMissingActionCase() { + basicUrl.replace("&action=verify", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testOtherActionCase() { + basicUrl.replace("&action=verify", "&action=confirm") + .toQrCodeData() + ?.action + ?.shouldBeEqualTo("confirm") + } + + @Test + fun testBadRequestEventId() { + basicUrl.replace("%24azertyazerty", "%32azertyazerty") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingUserId() { + basicUrl.replace("@benoit:matrix.org", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testBadUserId() { + basicUrl.replace("@benoit:matrix.org", "@benoit") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingSecret() { + basicUrl.replace("&secret=sharedSecret", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingOtherUserKey() { + basicUrl.replace("&other_user_key=otherUserKey", "") + .toQrCodeData() + .shouldBeNull() + } +} diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt index 9591feaa32..0773be5ad2 100644 --- a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/task/CoroutineSequencersTest.kt @@ -16,6 +16,8 @@ package im.vector.matrix.android.internal.task +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay diff --git a/vector/build.gradle b/vector/build.gradle index 26fad1bdaf..2b0296c9d0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -368,6 +368,10 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + // QR-code + implementation 'com.google.zxing:core:3.4.0' + implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt index 6624a05985..81708182b7 100644 --- a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.debug +import android.app.Activity import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context @@ -25,13 +26,38 @@ import androidx.core.app.NotificationCompat import androidx.core.app.Person import butterknife.OnClick import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions +import im.vector.riotx.core.utils.toast import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity +import im.vector.riotx.features.qrcode.QrCodeScannerActivity +import kotlinx.android.synthetic.debug.activity_debug_menu.* +import javax.inject.Inject class DebugMenuActivity : VectorBaseActivity() { override fun getLayoutRes() = R.layout.activity_debug_menu + @Inject + lateinit var activeSessionHolder: ActiveSessionHolder + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun initUiAndData() { + renderQrCode("https://www.example.org") + } + + private fun renderQrCode(text: String) { + debug_qr_code.setData(text, true) + } + @OnClick(R.id.debug_test_text_view_link) fun testTextViewLink() { startActivity(Intent(this, TestLinkifyActivity::class.java)) @@ -140,4 +166,37 @@ class DebugMenuActivity : VectorBaseActivity() { fun testCrash() { throw RuntimeException("Application crashed from user demand") } + + @OnClick(R.id.debug_scan_qr_code) + fun scanQRCode() { + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { + doScanQRCode() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && allGranted(grantResults)) { + doScanQRCode() + } + } + + private fun doScanQRCode() { + QrCodeScannerActivity.startForResult(this) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + QrCodeScannerActivity.QR_CODE_SCANNER_REQUEST_CODE -> { + toast("QrCode: " + QrCodeScannerActivity.getResultText(data) + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(data)) + + // Also update the current QR Code (reverse operation) + renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "") + } + } + } + } } diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index 01ab061f6a..6578258e70 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -61,6 +61,19 @@ android:layout_height="wrap_content" android:text="Crash the app" /> + + + + diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c6e4b51c44..3207ab257a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + - + + Copyright 2017 Sergiy Kovalchuk +
  • + ZXing +
    + Copyright 2007 ZXing authors +
  • +
  • + barcodescanner +
    + Copyright (c) 2014 Dushyanth Maguluru +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index 814c5f04c6..5cc0e071a8 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    @@ -49,6 +49,7 @@ import im.vector.riotx.features.login.LoginSplashFragment
     import im.vector.riotx.features.login.LoginWaitForEmailFragment
     import im.vector.riotx.features.login.LoginWebFragment
     import im.vector.riotx.features.login.terms.LoginTermsFragment
    +import im.vector.riotx.features.qrcode.QrCodeScannerFragment
     import im.vector.riotx.features.reactions.EmojiChooserFragment
     import im.vector.riotx.features.reactions.EmojiSearchResultFragment
     import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
    @@ -56,6 +57,8 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
     import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
     import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
     import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
    +import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment
    +import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
     import im.vector.riotx.features.roomprofile.RoomProfileFragment
     import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
     import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
    @@ -66,6 +69,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
     import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
     import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
     import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
    +import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
     import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
     import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
     import im.vector.riotx.features.settings.push.PushGatewaysFragment
    @@ -319,4 +323,24 @@ interface FragmentModule {
         @IntoMap
         @FragmentKey(VerificationConclusionFragment::class)
         fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(QrCodeScannerFragment::class)
    +    fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(DeviceListFragment::class)
    +    fun bindDeviceListFragment(fragment: DeviceListFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(DeviceTrustInfoActionFragment::class)
    +    fun bindDeviceTrustInfoActionFragment(fragment: DeviceTrustInfoActionFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(CrossSigningSettingsFragment::class)
    +    fun bindCrossSigningSettingsFragment(fragment: CrossSigningSettingsFragment): Fragment
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    index 2680bd15c1..f03f6cb784 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt
    @@ -24,11 +24,12 @@ import dagger.Component
     import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.preference.UserAvatarPreference
     import im.vector.riotx.features.MainActivity
    +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
    +import im.vector.riotx.features.debug.DebugMenuActivity
     import im.vector.riotx.features.home.HomeActivity
     import im.vector.riotx.features.home.HomeModule
    -import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
     import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
     import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
    @@ -43,6 +44,7 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity
     import im.vector.riotx.features.media.VideoMediaViewerActivity
     import im.vector.riotx.features.navigation.Navigator
     import im.vector.riotx.features.permalink.PermalinkHandlerActivity
    +import im.vector.riotx.features.qrcode.QrCodeScannerActivity
     import im.vector.riotx.features.rageshake.BugReportActivity
     import im.vector.riotx.features.rageshake.BugReporter
     import im.vector.riotx.features.rageshake.RageShake
    @@ -50,7 +52,9 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
     import im.vector.riotx.features.reactions.widget.ReactionButton
     import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
     import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
    +import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
     import im.vector.riotx.features.settings.VectorSettingsActivity
    +import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet
     import im.vector.riotx.features.share.IncomingShareActivity
     import im.vector.riotx.features.signout.soft.SoftLogoutActivity
     import im.vector.riotx.features.ui.UiStateRepository
    @@ -139,6 +143,14 @@ interface ScreenComponent {
     
         fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
     
    +    fun inject(activity: QrCodeScannerActivity)
    +
    +    fun inject(activity: DebugMenuActivity)
    +
    +    fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet)
    +
    +    fun inject(deviceListBottomSheet: DeviceListBottomSheet)
    +
         @Component.Factory
         interface Factory {
             fun create(vectorComponent: VectorComponent,
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt
    index 404570568d..9816a480e2 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt
    @@ -38,10 +38,16 @@ abstract class ProfileActionItem : VectorEpoxyModel()
         var subtitle: String? = null
         @EpoxyAttribute
         var iconRes: Int = 0
    +
    +    @EpoxyAttribute
    +    var editableRes: Int = R.drawable.ic_arrow_right
    +
         @EpoxyAttribute
         var editable: Boolean = true
    +
         @EpoxyAttribute
         var destructive: Boolean = false
    +
         @EpoxyAttribute
         var listener: View.OnClickListener? = null
     
    @@ -66,6 +72,13 @@ abstract class ProfileActionItem : VectorEpoxyModel()
             } else {
                 holder.icon.isVisible = false
             }
    +
    +        if (editableRes != 0) {
    +            holder.editable.setImageResource(editableRes)
    +            holder.editable.isVisible = true
    +        } else {
    +            holder.editable.isVisible = false
    +        }
         }
     
         class Holder : VectorEpoxyHolder() {
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt
    index ab68bdc4ce..7dce5e15d3 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt
    @@ -36,6 +36,7 @@ fun EpoxyController.buildProfileAction(
             subtitle: String? = null,
             editable: Boolean = true,
             @DrawableRes icon: Int = 0,
    +        @DrawableRes editableRes: Int? = null,
             destructive: Boolean = false,
             divider: Boolean = true,
             action: ClickListener? = null
    @@ -45,6 +46,9 @@ fun EpoxyController.buildProfileAction(
             id("action_$id")
             subtitle(subtitle)
             editable(editable)
    +        apply {
    +            editableRes?.let { editableRes(editableRes) }
    +        }
             destructive(destructive)
             title(title)
             listener { _ ->
    diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    index 15f541f72d..2f2ce130a8 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
    @@ -226,11 +226,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
             mainActivityStarted = true
     
             MainActivity.restartApp(this,
    -                                MainActivityArgs(
    -                                        clearCredentials = !globalError.softLogout,
    -                                        isUserLoggedOut = true,
    -                                        isSoftLogout = globalError.softLogout
    -                                )
    +                MainActivityArgs(
    +                        clearCredentials = !globalError.softLogout,
    +                        isUserLoggedOut = true,
    +                        isSoftLogout = globalError.softLogout
    +                )
             )
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/core/qrcode/QrCode.kt b/vector/src/main/java/im/vector/riotx/core/qrcode/QrCode.kt
    new file mode 100644
    index 0000000000..110427a3ec
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/qrcode/QrCode.kt
    @@ -0,0 +1,45 @@
    +/*
    + * Copyright 2020 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.riotx.core.qrcode
    +
    +import android.graphics.Bitmap
    +import android.graphics.Color
    +import androidx.annotation.ColorInt
    +import com.google.zxing.BarcodeFormat
    +import com.google.zxing.common.BitMatrix
    +import com.google.zxing.qrcode.QRCodeWriter
    +
    +fun String.toBitMatrix(size: Int): BitMatrix {
    +    return QRCodeWriter().encode(
    +            this,
    +            BarcodeFormat.QR_CODE,
    +            size,
    +            size
    +    )
    +}
    +
    +fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
    +                       @ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
    +    val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    +    for (x in 0 until width) {
    +        for (y in 0 until height) {
    +            bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor)
    +        }
    +    }
    +
    +    return bmp
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt
    index 02869fa420..3f1c134d5d 100644
    --- a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericFooterItem.kt
    @@ -15,13 +15,16 @@
      */
     package im.vector.riotx.core.ui.list
     
    +import android.view.Gravity
     import android.widget.TextView
    +import androidx.annotation.ColorInt
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
     import im.vector.riotx.core.extensions.setTextOrHide
    +import im.vector.riotx.features.themes.ThemeUtils
     
     /**
      * A generic list item.
    @@ -41,12 +44,26 @@ abstract class GenericFooterItem : VectorEpoxyModel()
         @EpoxyAttribute
         var itemClickAction: GenericItem.Action? = null
     
    +    @EpoxyAttribute
    +    var centered: Boolean = true
    +
    +    @EpoxyAttribute
    +    @ColorInt
    +    var textColor: Int? = null
    +
         override fun bind(holder: Holder) {
             holder.text.setTextOrHide(text)
             when (style) {
                 GenericItem.STYLE.BIG_TEXT    -> holder.text.textSize = 18f
                 GenericItem.STYLE.NORMAL_TEXT -> holder.text.textSize = 14f
             }
    +        holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START
    +
    +        if (textColor != null) {
    +            holder.text.setTextColor(textColor!!)
    +        } else {
    +            holder.text.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary))
    +        }
     
             holder.view.setOnClickListener {
                 itemClickAction?.perform?.run()
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    index c936ba8164..fc622f4dcb 100644
    --- a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItem.kt
    @@ -60,6 +60,10 @@ abstract class GenericItem : VectorEpoxyModel() {
         @DrawableRes
         var endIconResourceId: Int = -1
     
    +    @EpoxyAttribute
    +    @DrawableRes
    +    var titleIconResourceId: Int = -1
    +
         @EpoxyAttribute
         var hasIndeterminateProcess = false
     
    @@ -72,6 +76,13 @@ abstract class GenericItem : VectorEpoxyModel() {
         override fun bind(holder: Holder) {
             holder.titleText.setTextOrHide(title)
     
    +        if (titleIconResourceId != -1) {
    +            holder.titleIcon.setImageResource(titleIconResourceId)
    +            holder.titleIcon.isVisible = true
    +        } else {
    +            holder.titleIcon.isVisible = false
    +        }
    +
             when (style) {
                 STYLE.BIG_TEXT    -> holder.titleText.textSize = 18f
                 STYLE.NORMAL_TEXT -> holder.titleText.textSize = 14f
    @@ -104,7 +115,7 @@ abstract class GenericItem : VectorEpoxyModel() {
     
         class Holder : VectorEpoxyHolder() {
             val root by bind(R.id.item_generic_root)
    -
    +        val titleIcon by bind(R.id.item_generic_title_image)
             val titleText by bind(R.id.item_generic_title_text)
             val descriptionText by bind(R.id.item_generic_description_text)
             val accessoryImage by bind(R.id.item_generic_accessory_image)
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt
    new file mode 100644
    index 0000000000..37c65aa4cc
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/list/GenericItemWithValue.kt
    @@ -0,0 +1,85 @@
    +/*
    + * Copyright 2019 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.riotx.core.ui.list
    +
    +import android.view.View
    +import android.widget.ImageView
    +import android.widget.TextView
    +import androidx.annotation.ColorInt
    +import androidx.annotation.DrawableRes
    +import androidx.core.view.isVisible
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.extensions.setTextOrHide
    +import im.vector.riotx.core.utils.DebouncedClickListener
    +import im.vector.riotx.features.themes.ThemeUtils
    +
    +/**
    + * A generic list item.
    + * Displays an item with a title, and optional description.
    + * Can display an accessory on the right, that can be an image or an indeterminate progress.
    + * If provided with an action, will display a button at the bottom of the list item.
    + */
    +@EpoxyModelClass(layout = R.layout.item_generic_with_value)
    +abstract class GenericItemWithValue : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var title: CharSequence? = null
    +
    +    @EpoxyAttribute
    +    var value: CharSequence? = null
    +
    +    @EpoxyAttribute
    +    @ColorInt
    +    var valueColorInt: Int? = null
    +
    +    @EpoxyAttribute
    +    @DrawableRes
    +    var titleIconResourceId: Int = -1
    +
    +    @EpoxyAttribute
    +    var itemClickAction: View.OnClickListener? = null
    +
    +    override fun bind(holder: Holder) {
    +        holder.titleText.setTextOrHide(title)
    +
    +        if (titleIconResourceId != -1) {
    +            holder.titleIcon.setImageResource(titleIconResourceId)
    +            holder.titleIcon.isVisible = true
    +        } else {
    +            holder.titleIcon.isVisible = false
    +        }
    +
    +        holder.valueText.setTextOrHide(value)
    +
    +        if (valueColorInt != null) {
    +            holder.valueText.setTextColor(valueColorInt!!)
    +        } else {
    +            holder.valueText.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary))
    +        }
    +
    +        holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) })
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val titleIcon by bind(R.id.itemGenericWithValueTitleIcon)
    +        val titleText by bind(R.id.itemGenericWithValueLabelText)
    +        val valueText by bind(R.id.itemGenericWithValueValueText)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/QrCodeImageView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/QrCodeImageView.kt
    new file mode 100644
    index 0000000000..7cf54ae588
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/QrCodeImageView.kt
    @@ -0,0 +1,99 @@
    +/*
    + * Copyright 2020 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.riotx.core.ui.views
    +
    +import android.content.Context
    +import android.graphics.Color
    +import android.graphics.drawable.AnimationDrawable
    +import android.graphics.drawable.BitmapDrawable
    +import android.util.AttributeSet
    +import androidx.appcompat.widget.AppCompatImageView
    +import im.vector.riotx.core.qrcode.toBitMatrix
    +import im.vector.riotx.core.qrcode.toBitmap
    +import kotlin.random.Random
    +
    +class QrCodeImageView @JvmOverloads constructor(
    +        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    +) : AppCompatImageView(context, attrs, defStyleAttr) {
    +
    +    private var data: String? = null
    +    private var animate = false
    +
    +    init {
    +        setBackgroundColor(Color.WHITE)
    +    }
    +
    +    fun setData(data: String, animate: Boolean) {
    +        this.data = data
    +        this.animate = animate
    +
    +        render()
    +    }
    +
    +    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    +        super.onSizeChanged(w, h, oldw, oldh)
    +        render()
    +    }
    +
    +    private fun render() {
    +        data
    +                ?.takeIf { height > 0 }
    +                ?.let {
    +                    if (animate) {
    +                        // NOT SUPPORTED YET val anim = createAnimation(it)
    +                        // NOT SUPPORTED YET setImageDrawable(anim)
    +                        // NOT SUPPORTED YET anim.start()
    +                        // NOT SUPPORTED YET setImageDrawable(BitmapDrawable(resources, it.toBitMatrix(height).toBitmap()))
    +                        val bitmap = it.toBitMatrix(height).toBitmap()
    +                        post { setImageBitmap(bitmap) }
    +                    } else {
    +                        val bitmap = it.toBitMatrix(height).toBitmap()
    +                        post { setImageBitmap(bitmap) }
    +                    }
    +                }
    +    }
    +
    +    private fun createAnimation(data: String): AnimationDrawable {
    +        val finalQr = data.toBitMatrix(height)
    +
    +        val list = mutableListOf(finalQr)
    +
    +        val random = Random(System.currentTimeMillis())
    +        val repeatTime = 8
    +        repeat(repeatTime) { index ->
    +            val alteredQr = finalQr.clone()
    +            for (x in 0 until alteredQr.width) {
    +                for (y in 0 until alteredQr.height) {
    +                    if (random.nextInt(repeatTime - index) == 0) {
    +                        // Pb is that it does not toggle a whole black square, but only a pixel
    +                        alteredQr.unset(x, y)
    +                    }
    +                }
    +            }
    +            list.add(alteredQr)
    +        }
    +
    +        val animDrawable = AnimationDrawable()
    +
    +        list.asReversed()
    +                .forEach {
    +                    animDrawable.addFrame(BitmapDrawable(resources, it.toBitmap()), 150)
    +                }
    +
    +        return animDrawable
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    index 1af7d2fbd1..856c71f888 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    @@ -23,12 +23,14 @@ import android.content.Context
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
     import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
     import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
     import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
    -import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
    +import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
    @@ -54,7 +56,7 @@ import kotlin.collections.HashMap
     @Singleton
     class KeyRequestHandler @Inject constructor(private val context: Context)
         : RoomKeysRequestListener,
    -        SasVerificationService.SasVerificationListener {
    +        VerificationService.VerificationListener {
     
         private val alertsToRequests = HashMap>()
     
    @@ -62,12 +64,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
     
         fun start(session: Session) {
             this.session = session
    -        session.getSasVerificationService().addListener(this)
    +        session.getVerificationService().addListener(this)
             session.addRoomKeysRequestListener(this)
         }
     
         fun stop() {
    -        session?.getSasVerificationService()?.removeListener(this)
    +        session?.getVerificationService()?.removeListener(this)
             session?.removeRoomKeysRequestListener(this)
             session = null
         }
    @@ -88,7 +90,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
             }
     
             // Do we already have alerts for this user/device
    -        val mappingKey = keyForMap(deviceId, userId)
    +        val mappingKey = keyForMap(userId, deviceId)
             if (alertsToRequests.containsKey(mappingKey)) {
                 // just add the request, there is already an alert for this
                 alertsToRequests[mappingKey]?.add(request)
    @@ -98,8 +100,8 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
             alertsToRequests[mappingKey] = ArrayList().apply { this.add(request) }
     
             // Add a notification for every incoming request
    -        session?.downloadKeys(listOf(userId), false, object : MatrixCallback> {
    -            override fun onSuccess(data: MXUsersDevicesMap) {
    +        session?.downloadKeys(listOf(userId), false, object : MatrixCallback> {
    +            override fun onSuccess(data: MXUsersDevicesMap) {
                     val deviceInfo = data.getObject(userId, deviceId)
     
                     if (null == deviceInfo) {
    @@ -109,9 +111,9 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
                     }
     
                     if (deviceInfo.isUnknown) {
    -                    session?.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED, deviceId, userId)
    +                    session?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
     
    -                    deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
    +                    deviceInfo.trustLevel = DeviceTrustLevel(false, false)
     
                         // can we get more info on this device?
                         session?.getDevicesList(object : MatrixCallback {
    @@ -143,7 +145,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
                               userId: String,
                               deviceId: String,
                               wasNewDevice: Boolean,
    -                          deviceInfo: MXDeviceInfo?,
    +                          deviceInfo: CryptoDeviceInfo?,
                               moreInfo: DeviceInfo? = null) {
             val deviceName = if (deviceInfo!!.displayName().isNullOrEmpty()) deviceInfo.deviceId else deviceInfo.displayName()
             val dialogText: String?
    @@ -180,7 +182,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
             }
     
             val alert = PopupAlertManager.VectorAlert(
    -                alertManagerId(deviceId, userId),
    +                alertManagerId(userId, deviceId),
                     context.getString(R.string.key_share_request),
                     dialogText,
                     R.drawable.key_small
    @@ -188,7 +190,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
     
             alert.colorRes = R.color.key_share_req_accent_color
     
    -        val mappingKey = keyForMap(deviceId, userId)
    +        val mappingKey = keyForMap(userId, deviceId)
             alert.dismissedAction = Runnable {
                 denyAllRequests(mappingKey)
             }
    @@ -248,7 +250,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
                 return
             }
     
    -        val alertMgrUniqueKey = alertManagerId(deviceId, userId)
    +        val alertMgrUniqueKey = alertManagerId(userId, deviceId)
             alertsToRequests[alertMgrUniqueKey]?.removeAll {
                 it.deviceId == request.deviceId
                         && it.userId == request.userId
    @@ -256,29 +258,33 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
             }
             if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) {
                 PopupAlertManager.cancelAlert(alertMgrUniqueKey)
    -            alertsToRequests.remove(keyForMap(deviceId, userId))
    +            alertsToRequests.remove(keyForMap(userId, deviceId))
             }
         }
     
    -    override fun transactionCreated(tx: SasVerificationTransaction) {
    +    override fun transactionCreated(tx: VerificationTransaction) {
         }
     
    -    override fun transactionUpdated(tx: SasVerificationTransaction) {
    -        val state = tx.state
    -        if (state == SasVerificationTxState.Verified) {
    -            // ok it's verified, see if we have key request for that
    -            shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
    -            PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
    +    override fun transactionUpdated(tx: VerificationTransaction) {
    +        if (tx is SasVerificationTransaction) {
    +            val state = tx.state
    +            if (state == VerificationTxState.Verified) {
    +                // ok it's verified, see if we have key request for that
    +                shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
    +                PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
    +            }
             }
    +        // should do it with QR tx also
    +        // TODO -> Probably better to listen to device trust changes?
         }
     
         override fun markedAsManuallyVerified(userId: String, deviceId: String) {
             // accept related requests
    -        shareAllSessions(keyForMap(deviceId, userId))
    -        PopupAlertManager.cancelAlert(alertManagerId(deviceId, userId))
    +        shareAllSessions(keyForMap(userId, deviceId))
    +        PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
         }
     
    -    private fun keyForMap(deviceId: String, userId: String) = "$deviceId$userId"
    +    private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId"
     
    -    private fun alertManagerId(deviceId: String, userId: String) = "ikr_$deviceId$userId"
    +    private fun alertManagerId(userId: String, deviceId: String) = "ikr_$deviceId$userId"
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    index 0bc9a3e144..fae7037403 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    @@ -18,5 +18,12 @@ package im.vector.riotx.features.crypto.verification
     
     import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
     
    -// TODO Add support for SCAN (QR code)
    -val supportedVerificationMethods = listOf(VerificationMethod.SAS)
    +val supportedVerificationMethods =
    +        listOf(
    +                // RiotX supports SAS verification
    +                VerificationMethod.SAS,
    +                // RiotX is able to show QR codes
    +                VerificationMethod.QR_CODE_SHOW,
    +                // RiotX is able to scan QR codes
    +                VerificationMethod.QR_CODE_SCAN
    +        )
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    index b59e2d3f8c..234b60cba3 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt
    @@ -17,9 +17,9 @@ package im.vector.riotx.features.crypto.verification
     
     import android.content.Context
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorBaseActivity
    @@ -34,73 +34,71 @@ import javax.inject.Singleton
      * Listens to the VerificationManager and add a new notification when an incoming request is detected.
      */
     @Singleton
    -class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : SasVerificationService.SasVerificationListener {
    +class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.VerificationListener {
     
         private var session: Session? = null
     
         fun start(session: Session) {
             this.session = session
    -        session.getSasVerificationService().addListener(this)
    +        session.getVerificationService().addListener(this)
         }
     
         fun stop() {
    -        session?.getSasVerificationService()?.removeListener(this)
    +        session?.getVerificationService()?.removeListener(this)
             this.session = null
         }
     
    -    override fun transactionCreated(tx: SasVerificationTransaction) {}
    +    override fun transactionCreated(tx: VerificationTransaction) {}
     
    -    override fun transactionUpdated(tx: SasVerificationTransaction) {
    +    override fun transactionUpdated(tx: VerificationTransaction) {
    +        if (!tx.isToDeviceTransport()) return
    +        // TODO maybe check also if
             when (tx.state) {
    -            SasVerificationTxState.OnStarted -> {
    -//                // Add a notification for every incoming request
    -//                val name = session?.getUser(tx.otherUserId)?.displayName
    -//                        ?: tx.otherUserId
    -//
    -//                val alert = PopupAlertManager.VectorAlert(
    -//                        "kvr_${tx.transactionId}",
    -//                        context.getString(R.string.sas_incoming_request_notif_title),
    -//                        context.getString(R.string.sas_incoming_request_notif_content, name),
    -//                        R.drawable.shield)
    -//                        .apply {
    -//                            contentAction = Runnable {
    -//                                val intent = SASVerificationActivity.incomingIntent(context,
    -//                                        session?.myUserId  ?: "",
    -//                                        tx.otherUserId,
    -//                                        tx.transactionId)
    -//                                weakCurrentActivity?.get()?.startActivity(intent)
    -//                            }
    -//                            dismissedAction = Runnable {
    -//                                tx.cancel()
    -//                            }
    -//                            addButton(
    -//                                    context.getString(R.string.ignore),
    -//                                    Runnable {
    -//                                        tx.cancel()
    -//                                    }
    -//                            )
    -//                            addButton(
    -//                                    context.getString(R.string.action_open),
    -//                                    Runnable {
    -//                                        val intent = SASVerificationActivity.incomingIntent(context,
    -//                                                session?.myUserId ?: "",
    -//                                                tx.otherUserId,
    -//                                                tx.transactionId)
    -//                                        weakCurrentActivity?.get()?.startActivity(intent)
    -//                                    }
    -//                            )
    -//                            // 10mn expiration
    -//                            expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
    -//                        }
    -//                PopupAlertManager.postVectorAlert(alert)
    +            VerificationTxState.OnStarted -> {
    +                // Add a notification for every incoming request
    +                val name = session?.getUser(tx.otherUserId)?.displayName
    +                        ?: tx.otherUserId
    +
    +                val alert = PopupAlertManager.VectorAlert(
    +                        "kvr_${tx.transactionId}",
    +                        context.getString(R.string.sas_incoming_request_notif_title),
    +                        context.getString(R.string.sas_incoming_request_notif_content, name),
    +                        R.drawable.shield)
    +                        .apply {
    +                            contentAction = Runnable {
    +                                (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                    it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
    +                                }
    +                            }
    +                            dismissedAction = Runnable {
    +                                tx.cancel()
    +                            }
    +                            addButton(
    +                                    context.getString(R.string.ignore),
    +                                    Runnable {
    +                                        tx.cancel()
    +                                    }
    +                            )
    +                            addButton(
    +                                    context.getString(R.string.action_open),
    +                                    Runnable {
    +                                        (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                            it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
    +                                        }
    +                                    }
    +                            )
    +                            // 10mn expiration
    +                            expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L)
    +                        }
    +                PopupAlertManager.postVectorAlert(alert)
                 }
    -            SasVerificationTxState.Cancelled,
    -            SasVerificationTxState.OnCancelled,
    -            SasVerificationTxState.Verified  -> {
    +            VerificationTxState.Cancelled,
    +            VerificationTxState.OnCancelled,
    +            VerificationTxState.Verified  -> {
                     // cancel related notification
                     PopupAlertManager.cancelAlert("kvr_${tx.transactionId}")
                 }
    -            else                             -> Unit
    +            else                          -> Unit
             }
         }
     
    @@ -132,7 +130,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
                                 }
                             }
                             dismissedAction = Runnable {
    -                            session?.getSasVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
    +                            session?.getVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
                                         pr.requestInfo?.fromDevice ?: "",
                                         pr.transactionId ?: "",
                                         pr.roomId ?: ""
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    new file mode 100644
    index 0000000000..83fdc46270
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    @@ -0,0 +1,28 @@
    +/*
    + * Copyright 2020 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.riotx.features.crypto.verification
    +
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +
    +sealed class VerificationAction : VectorViewModelAction {
    +    data class RequestVerificationByDM(val otherUserId: String, val roomId: String?) : VerificationAction()
    +    data class StartSASVerification(val otherUserId: String, val pendingRequestTransactionId: String) : VerificationAction()
    +    data class RemoteQrCodeScanned(val otherUserId: String, val transactionId: String, val scannedData: String) : VerificationAction()
    +    data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
    +    data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
    +    object GotItConclusion : VerificationAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    index 22369f37b5..702e79d451 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    @@ -31,7 +31,7 @@ import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ScreenComponent
     import im.vector.riotx.core.extensions.commitTransactionNow
    @@ -40,7 +40,6 @@ import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMet
     import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
     import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
     import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
    -import im.vector.riotx.features.crypto.verification.request.VerificationRequestViewModel
     import im.vector.riotx.features.home.AvatarRenderer
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.bottom_sheet_verification.*
    @@ -57,8 +56,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
                 val roomId: String? = null
         ) : Parcelable
     
    -    @Inject
    -    lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory
         @Inject
         lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
         @Inject
    @@ -101,7 +98,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
             it.otherUserMxItem?.let { matrixItem ->
                 avatarRenderer.render(matrixItem, otherUserAvatarImageView)
     
    -            if(it.sasTransactionState == SasVerificationTxState.Verified) {
    +            if (it.sasTransactionState == VerificationTxState.Verified || it.qrTransactionState == VerificationTxState.Verified) {
                     otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName())
                     otherUserShield.isVisible = true
                 } else {
    @@ -113,33 +110,35 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
             // Did the request result in a SAS transaction?
             if (it.sasTransactionState != null) {
                 when (it.sasTransactionState) {
    -                SasVerificationTxState.None,
    -                SasVerificationTxState.SendingStart,
    -                SasVerificationTxState.Started,
    -                SasVerificationTxState.OnStarted,
    -                SasVerificationTxState.SendingAccept,
    -                SasVerificationTxState.Accepted,
    -                SasVerificationTxState.OnAccepted,
    -                SasVerificationTxState.SendingKey,
    -                SasVerificationTxState.KeySent,
    -                SasVerificationTxState.OnKeyReceived,
    -                SasVerificationTxState.ShortCodeReady,
    -                SasVerificationTxState.ShortCodeAccepted,
    -                SasVerificationTxState.SendingMac,
    -                SasVerificationTxState.MacSent,
    -                SasVerificationTxState.Verifying   -> {
    +                VerificationTxState.None,
    +                VerificationTxState.SendingStart,
    +                VerificationTxState.Started,
    +                VerificationTxState.OnStarted,
    +                VerificationTxState.SendingAccept,
    +                VerificationTxState.Accepted,
    +                VerificationTxState.OnAccepted,
    +                VerificationTxState.SendingKey,
    +                VerificationTxState.KeySent,
    +                VerificationTxState.OnKeyReceived,
    +                VerificationTxState.ShortCodeReady,
    +                VerificationTxState.ShortCodeAccepted,
    +                VerificationTxState.SendingMac,
    +                VerificationTxState.MacSent,
    +                VerificationTxState.Verifying   -> {
                         showFragment(VerificationEmojiCodeFragment::class, Bundle().apply {
                             putParcelable(MvRx.KEY_ARG, VerificationArgs(
                                     it.otherUserMxItem?.id ?: "",
    -                                it.pendingRequest?.transactionId))
    +                                // If it was outgoing it.transaction id would be null, but the pending request
    +                                // would be updated (from localID to txId)
    +                                it.pendingRequest.invoke()?.transactionId ?: it.transactionId))
                         })
                     }
    -                SasVerificationTxState.Verified,
    -                SasVerificationTxState.Cancelled,
    -                SasVerificationTxState.OnCancelled -> {
    +                VerificationTxState.Verified,
    +                VerificationTxState.Cancelled,
    +                VerificationTxState.OnCancelled -> {
                         showFragment(VerificationConclusionFragment::class, Bundle().apply {
                             putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
    -                                it.sasTransactionState == SasVerificationTxState.Verified,
    +                                it.sasTransactionState == VerificationTxState.Verified,
                                     it.cancelCode?.value))
                         })
                     }
    @@ -148,39 +147,53 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
                 return@withState
             }
     
    -        // At this point there is no transaction for this request
    +        when (it.qrTransactionState) {
    +            VerificationTxState.Verified,
    +            VerificationTxState.Cancelled,
    +            VerificationTxState.OnCancelled -> {
    +                showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
    +                            it.qrTransactionState == VerificationTxState.Verified,
    +                            it.cancelCode?.value))
    +                })
    +                return@withState
    +            }
    +            else                            -> Unit
    +        }
    +
    +        // At this point there is no SAS transaction for this request
     
             // Transaction has not yet started
    -        if (it.pendingRequest?.cancelConclusion != null) {
    +        if (it.pendingRequest.invoke()?.cancelConclusion != null) {
                 // The request has been declined, we should dismiss
                 dismiss()
             }
     
             // If it's an outgoing
    -        if (it.pendingRequest == null || !it.pendingRequest.isIncoming) {
    +        if (it.pendingRequest.invoke() == null || it.pendingRequest.invoke()?.isIncoming == false) {
                 Timber.v("## SAS show bottom sheet for outgoing request")
    -            if (it.pendingRequest?.isReady == true) {
    +            if (it.pendingRequest.invoke()?.isReady == true) {
                     Timber.v("## SAS show bottom sheet for outgoing and ready request")
                     // Show choose method fragment with waiting
                     showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
                         putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
    -                            ?: "", it.pendingRequest.transactionId))
    +                            ?: "", it.pendingRequest.invoke()?.transactionId))
                     })
                 } else {
                     // Stay on the start fragment
                     showFragment(VerificationRequestFragment::class, Bundle().apply {
                         putParcelable(MvRx.KEY_ARG, VerificationArgs(
                                 it.otherUserMxItem?.id ?: "",
    -                            it.pendingRequest?.transactionId,
    +                            it.pendingRequest.invoke()?.transactionId,
                                 it.roomId))
                     })
                 }
    -        } else if (it.pendingRequest.isIncoming) {
    +        } else if (it.pendingRequest.invoke()?.isIncoming == true) {
                 Timber.v("## SAS show bottom sheet for Incoming request")
                 // For incoming we can switch to choose method because ready is being sent or already sent
                 showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
                     putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
    -                        ?: "", it.pendingRequest.transactionId))
    +                        ?: "", it.pendingRequest.invoke()?.transactionId))
                 })
             }
             super.invalidate()
    @@ -204,10 +217,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
     
         companion object {
    -        fun withArgs(roomId: String, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
    +        fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
                 return VerificationBottomSheet().apply {
                     arguments = Bundle().apply {
    -                    putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
    +                    putParcelable(MvRx.KEY_ARG, VerificationArgs(
                                 otherUserId = otherUserId,
                                 roomId = roomId,
                                 verificationId = transactionId
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 7f897f31f3..465605efc6 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -17,40 +17,52 @@ package im.vector.riotx.features.crypto.verification
     
     import androidx.lifecycle.LiveData
     import androidx.lifecycle.MutableLiveData
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
    +import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.*
    +import im.vector.matrix.android.api.session.crypto.sas.CancelCode
    +import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.events.model.LocalEcho
    +import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.core.di.HasScreenInjector
     import im.vector.riotx.core.platform.EmptyViewEvents
     import im.vector.riotx.core.platform.VectorViewModel
    -import im.vector.riotx.core.platform.VectorViewModelAction
     import im.vector.riotx.core.utils.LiveEvent
    +import timber.log.Timber
     
     data class VerificationBottomSheetViewState(
             val otherUserMxItem: MatrixItem? = null,
             val roomId: String? = null,
    -        val pendingRequest: PendingVerificationRequest? = null,
    -        val sasTransactionState: SasVerificationTxState? = null,
    +        val pendingRequest: Async = Uninitialized,
    +        val pendingLocalId: String? = null,
    +        val sasTransactionState: VerificationTxState? = null,
    +        val qrTransactionState: VerificationTxState? = null,
    +        val transactionId: String? = null,
             val cancelCode: CancelCode? = null
     ) : MvRxState
     
    -sealed class VerificationAction : VectorViewModelAction {
    -    data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
    -    data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
    -    data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    -    data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    -    object GotItConclusion : VerificationAction()
    -}
    -
     class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
                                                                        private val session: Session)
         : VectorViewModel(initialState),
    -        SasVerificationService.SasVerificationListener {
    +        VerificationService.VerificationListener {
     
         // Can be used for several actions, for a one shot result
         private val _requestLiveData = MutableLiveData>>()
    @@ -58,11 +70,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
             get() = _requestLiveData
     
         init {
    -        session.getSasVerificationService().addListener(this)
    +        session.getVerificationService().addListener(this)
         }
     
         override fun onCleared() {
    -        session.getSasVerificationService().removeListener(this)
    +        session.getVerificationService().removeListener(this)
             super.onCleared()
         }
     
    @@ -81,16 +93,22 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
     
                 val userItem = session.getUser(args.otherUserId)
     
    -            val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    +            val pr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
     
    -            val sasTx = pr?.transactionId?.let {
    -                session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
    +            val sasTx = (pr?.transactionId ?: args.verificationId)?.let {
    +                session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction
    +            }
    +
    +            val qrTx = (pr?.transactionId ?: args.verificationId)?.let {
    +                session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
                 }
     
                 return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
                         otherUserMxItem = userItem?.toMatrixItem(),
                         sasTransactionState = sasTx?.state,
    -                    pendingRequest = pr,
    +                    qrTransactionState = qrTx?.state,
    +                    transactionId = args.verificationId,
    +                    pendingRequest = if (pr != null) Success(pr) else Uninitialized,
                         roomId = args.roomId)
                 )
             }
    @@ -100,20 +118,58 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
             val otherUserId = state.otherUserMxItem?.id ?: return@withState
             val roomId = state.roomId
                     ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
    -                ?: return@withState
    +
             when (action) {
                 is VerificationAction.RequestVerificationByDM -> {
    -//                session
    -                setState {
    -                    copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, roomId))
    +                if (roomId == null) {
    +                    val localID = LocalEcho.createLocalEchoId()
    +                    setState {
    +                        copy(
    +                                pendingLocalId = localID,
    +                                pendingRequest = Loading()
    +                        )
    +                    }
    +                    val roomParams = CreateRoomParams().apply {
    +                        invitedUserIds = listOf(otherUserId).toMutableList()
    +                        setDirectMessage()
    +                    }
    +                    session.createRoom(roomParams, object : MatrixCallback {
    +                        override fun onSuccess(data: String) {
    +                            setState {
    +                                copy(
    +                                        roomId = data,
    +                                        pendingRequest = Success(
    +                                                session
    +                                                        .getVerificationService()
    +                                                        .requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, data, pendingLocalId)
    +                                        )
    +                                )
    +                            }
    +                        }
    +
    +                        override fun onFailure(failure: Throwable) {
    +                            setState {
    +                                copy(pendingRequest = Fail(failure))
    +                            }
    +                        }
    +                    })
    +                } else {
    +                    setState {
    +                        copy(
    +                                pendingRequest = Success(session
    +                                        .getVerificationService()
    +                                        .requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, roomId)
    +                                )
    +                        )
    +                    }
                     }
                 }
                 is VerificationAction.StartSASVerification    -> {
    -                val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
    +                val request = session.getVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
                             ?: return@withState
    -
    +                if (roomId == null) return@withState
                     val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
    -                session.getSasVerificationService().beginKeyVerificationInDMs(
    +                session.getVerificationService().beginKeyVerificationInDMs(
                             VerificationMethod.SAS,
                             transactionId = action.pendingRequestTransactionId,
                             roomId = roomId,
    @@ -122,14 +178,26 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                             callback = null
                     )
                 }
    +            is VerificationAction.RemoteQrCodeScanned     -> {
    +                val existingTransaction = session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
    +                existingTransaction
    +                        ?.userHasScannedOtherQrCode(action.scannedData)
    +                        ?.let { cancelCode ->
    +                            // Something went wrong
    +                            Timber.w("## Something is not right: $cancelCode")
    +                            // TODO
    +                        }
    +            }
                 is VerificationAction.SASMatchAction          -> {
    -                session.getSasVerificationService()
    -                        .getExistingTransaction(action.userID, action.sasTransactionId)
    -                        ?.userHasVerifiedShortCode()
    +                (session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.sasTransactionId)
    +                        as? SasVerificationTransaction)?.userHasVerifiedShortCode()
                 }
                 is VerificationAction.SASDoNotMatchAction     -> {
    -                session.getSasVerificationService()
    -                        .getExistingTransaction(action.userID, action.sasTransactionId)
    +                (session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.sasTransactionId)
    +                        as? SasVerificationTransaction)
                             ?.shortCodeDoesNotMatch()
                 }
                 is VerificationAction.GotItConclusion         -> {
    @@ -138,18 +206,33 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
             }
         }
     
    -    override fun transactionCreated(tx: SasVerificationTransaction) {
    +    override fun transactionCreated(tx: VerificationTransaction) {
             transactionUpdated(tx)
         }
     
    -    override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
    -        if (tx.transactionId == state.pendingRequest?.transactionId) {
    -            // A SAS tx has been started following this request
    -            setState {
    -                copy(
    -                        sasTransactionState = tx.state,
    -                        cancelCode = tx.cancelledReason
    -                )
    +    override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    +        when (tx) {
    +            is SasVerificationTransaction    -> {
    +                if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
    +                    // A SAS tx has been started following this request
    +                    setState {
    +                        copy(
    +                                sasTransactionState = tx.state,
    +                                cancelCode = tx.cancelledReason
    +                        )
    +                    }
    +                }
    +            }
    +            is QrCodeVerificationTransaction -> {
    +                if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
    +                    // A SAS tx has been started following this request
    +                    setState {
    +                        copy(
    +                                qrTransactionState = tx.state,
    +                                cancelCode = tx.cancelledReason
    +                        )
    +                    }
    +                }
                 }
             }
         }
    @@ -160,9 +243,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
     
         override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
     
    -        if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) {
    +        if (pr.localID == state.pendingLocalId
    +                || pr.localID == state.pendingRequest.invoke()?.localID
    +                || state.pendingRequest.invoke()?.transactionId == pr.transactionId) {
                 setState {
    -                copy(pendingRequest = pr)
    +                copy(pendingRequest = Success(pr))
                 }
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    index 8760a8603e..87bb843291 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    @@ -22,8 +22,8 @@ import im.vector.riotx.core.epoxy.dividerItem
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    -import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationBigImageItem
     import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem
     import javax.inject.Inject
     
     class VerificationChooseMethodController @Inject constructor(
    @@ -43,33 +43,37 @@ class VerificationChooseMethodController @Inject constructor(
         override fun buildModels() {
             val state = viewState ?: return
     
    -        if (state.QRModeAvailable) {
    +        if (state.otherCanScanQrCode || state.otherCanShowQrCode) {
                 bottomSheetVerificationNoticeItem {
                     id("notice")
                     notice(stringProvider.getString(R.string.verification_scan_notice))
                 }
     
    -            // TODO Generate the QR code
    -            bottomSheetVerificationBigImageItem {
    -                id("qr")
    -                imageRes(R.drawable.riotx_logo)
    +            if (state.otherCanScanQrCode && !state.qrCodeText.isNullOrBlank()) {
    +                bottomSheetVerificationQrCodeItem {
    +                    id("qr")
    +                    data(state.qrCodeText)
    +                    animate(false)
    +                }
    +
    +                dividerItem {
    +                    id("sep0")
    +                }
                 }
     
    -            dividerItem {
    -                id("sep0")
    -            }
    +            if (state.otherCanShowQrCode) {
    +                bottomSheetVerificationActionItem {
    +                    id("openCamera")
    +                    title(stringProvider.getString(R.string.verification_scan_their_code))
    +                    titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                    iconRes(R.drawable.ic_camera)
    +                    iconColor(colorProvider.getColor(R.color.riotx_accent))
    +                    listener { listener?.openCamera() }
    +                }
     
    -            bottomSheetVerificationActionItem {
    -                id("openCamera")
    -                title(stringProvider.getString(R.string.verification_scan_their_code))
    -                titleColor(colorProvider.getColor(R.color.riotx_accent))
    -                iconRes(R.drawable.ic_camera)
    -                iconColor(colorProvider.getColor(R.color.riotx_accent))
    -                listener { listener?.openCamera() }
    -            }
    -
    -            dividerItem {
    -                id("sep1")
    +                dividerItem {
    +                    id("sep1")
    +                }
                 }
     
                 bottomSheetVerificationActionItem {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    index b782afca39..e0b7f97383 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    @@ -15,6 +15,8 @@
      */
     package im.vector.riotx.features.crypto.verification.choose
     
    +import android.app.Activity
    +import android.content.Intent
     import android.os.Bundle
     import android.view.View
     import com.airbnb.mvrx.fragmentViewModel
    @@ -24,9 +26,15 @@ import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.features.crypto.verification.VerificationAction
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import im.vector.riotx.features.qrcode.QrCodeScannerActivity
     import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import timber.log.Timber
     import javax.inject.Inject
     
     class VerificationChooseMethodFragment @Inject constructor(
    @@ -61,13 +69,54 @@ class VerificationChooseMethodFragment @Inject constructor(
             controller.update(state)
         }
     
    -    override fun doVerifyBySas() = withState(sharedViewModel) {
    +    override fun doVerifyBySas() = withState(sharedViewModel) { state ->
             sharedViewModel.handle(VerificationAction.StartSASVerification(
    -                it.otherUserMxItem?.id ?: "",
    -                it.pendingRequest?.transactionId ?: ""))
    +                state.otherUserMxItem?.id ?: "",
    +                state.pendingRequest.invoke()?.transactionId ?: ""))
         }
     
         override fun openCamera() {
    -        // TODO
    +        if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
    +            doOpenQRCodeScanner()
    +        }
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    +
    +        if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && allGranted(grantResults)) {
    +            doOpenQRCodeScanner()
    +        }
    +    }
    +
    +    private fun doOpenQRCodeScanner() {
    +        QrCodeScannerActivity.startForResult(this)
    +    }
    +
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        super.onActivityResult(requestCode, resultCode, data)
    +
    +        if (resultCode == Activity.RESULT_OK) {
    +            when (requestCode) {
    +                QrCodeScannerActivity.QR_CODE_SCANNER_REQUEST_CODE -> {
    +                    val scannedQrCode = QrCodeScannerActivity.getResultText(data)
    +                    val wasQrCode = QrCodeScannerActivity.getResultIsQrCode(data)
    +
    +                    if (wasQrCode && !scannedQrCode.isNullOrBlank()) {
    +                        onRemoteQrCodeScanned(scannedQrCode)
    +                    } else {
    +                        Timber.w("It was not a QR code, or empty result")
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) { state ->
    +        sharedViewModel.handle(VerificationAction.RemoteQrCodeScanned(
    +                state.otherUserMxItem?.id ?: "",
    +                state.pendingRequest.invoke()?.transactionId ?: "",
    +                remoteQrCode
    +        ))
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    index a6e85de13a..75c1b69058 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    @@ -22,9 +22,10 @@ import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
     import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.core.di.HasScreenInjector
     import im.vector.riotx.core.platform.EmptyAction
    @@ -35,28 +36,39 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     data class VerificationChooseMethodViewState(
             val otherUserId: String = "",
             val transactionId: String = "",
    -        val QRModeAvailable: Boolean = false,
    +        val otherCanShowQrCode: Boolean = false,
    +        val otherCanScanQrCode: Boolean = false,
    +        val qrCodeText: String? = null,
             val SASModeAvailable: Boolean = false
     ) : MvRxState
     
     class VerificationChooseMethodViewModel @AssistedInject constructor(
             @Assisted initialState: VerificationChooseMethodViewState,
             private val session: Session
    -) : VectorViewModel(initialState), SasVerificationService.SasVerificationListener {
    +) : VectorViewModel(initialState), VerificationService.VerificationListener {
     
    -    override fun transactionCreated(tx: SasVerificationTransaction) {}
    +    override fun transactionCreated(tx: VerificationTransaction) {
    +        transactionUpdated(tx)
    +    }
     
    -    override fun transactionUpdated(tx: SasVerificationTransaction) {}
    +    override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    +        if (tx.transactionId == state.transactionId && tx is QrCodeVerificationTransaction) {
    +            setState {
    +                copy(
    +                        qrCodeText = tx.qrCodeText
    +                )
    +            }
    +        }
    +    }
     
         override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
    -        val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
    -        val qrAvailable = pvr?.hasMethod(VerificationMethod.SCAN) ?: false
    -        val emojiAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
    +        val pvr = session.getVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
     
             setState {
                 copy(
    -                    QRModeAvailable = qrAvailable,
    -                    SASModeAvailable = emojiAvailable
    +                    otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
    +                    otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
    +                    SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
                 )
             }
         }
    @@ -67,12 +79,12 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
         }
     
         init {
    -        session.getSasVerificationService().addListener(this)
    +        session.getVerificationService().addListener(this)
         }
     
         override fun onCleared() {
             super.onCleared()
    -        session.getSasVerificationService().removeListener(this)
    +        session.getVerificationService().removeListener(this)
         }
     
         companion object : MvRxViewModelFactory {
    @@ -84,14 +96,17 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
             override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
                 val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
                 val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    -            val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    -            val qrAvailable = pvr?.hasMethod(VerificationMethod.SCAN) ?: false
    -            val emojiAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
    +            val pvr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    +
    +            // Get the QR code now, because transaction is already created, so transactionCreated() will not be called
    +            val qrCodeVerificationTransaction = session.getVerificationService().getExistingTransaction(args.otherUserId, args.verificationId ?: "")
     
                 return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
                         transactionId = args.verificationId ?: "",
    -                    QRModeAvailable = qrAvailable,
    -                    SASModeAvailable = emojiAvailable
    +                    otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
    +                    otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
    +                    qrCodeText = (qrCodeVerificationTransaction as? QrCodeVerificationTransaction)?.qrCodeText,
    +                    SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
                 )
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    index 9cf5c6466b..d2239e779f 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    @@ -43,6 +43,8 @@ class VerificationConclusionViewModel(initialState: VerificationConclusionViewSt
                 val args = viewModelContext.args()
     
                 return when (safeValueOf(args.cancelReason)) {
    +                CancelCode.QrCodeInvalid,
    +                CancelCode.MismatchedUser,
                     CancelCode.MismatchedSas,
                     CancelCode.MismatchedCommitment,
                     CancelCode.MismatchedKeys -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    index 20f123da04..8824cd88e2 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    @@ -20,9 +20,10 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
     import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.riotx.core.di.HasScreenInjector
    @@ -43,35 +44,35 @@ data class VerificationEmojiCodeViewState(
     class VerificationEmojiCodeViewModel @AssistedInject constructor(
             @Assisted initialState: VerificationEmojiCodeViewState,
             private val session: Session
    -) : VectorViewModel(initialState), SasVerificationService.SasVerificationListener {
    +) : VectorViewModel(initialState), VerificationService.VerificationListener {
     
         init {
             withState { state ->
    -            refreshStateFromTx(session.getSasVerificationService()
    +            refreshStateFromTx(session.getVerificationService()
                         .getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
    -                            ?: ""))
    +                            ?: "") as? SasVerificationTransaction)
             }
     
    -        session.getSasVerificationService().addListener(this)
    +        session.getVerificationService().addListener(this)
         }
     
         override fun onCleared() {
    -        session.getSasVerificationService().removeListener(this)
    +        session.getVerificationService().removeListener(this)
             super.onCleared()
         }
     
         private fun refreshStateFromTx(sasTx: SasVerificationTransaction?) {
             when (sasTx?.state) {
    -            SasVerificationTxState.None,
    -            SasVerificationTxState.SendingStart,
    -            SasVerificationTxState.Started,
    -            SasVerificationTxState.OnStarted,
    -            SasVerificationTxState.SendingAccept,
    -            SasVerificationTxState.Accepted,
    -            SasVerificationTxState.OnAccepted,
    -            SasVerificationTxState.SendingKey,
    -            SasVerificationTxState.KeySent,
    -            SasVerificationTxState.OnKeyReceived  -> {
    +            VerificationTxState.None,
    +            VerificationTxState.SendingStart,
    +            VerificationTxState.Started,
    +            VerificationTxState.OnStarted,
    +            VerificationTxState.SendingAccept,
    +            VerificationTxState.Accepted,
    +            VerificationTxState.OnAccepted,
    +            VerificationTxState.SendingKey,
    +            VerificationTxState.KeySent,
    +            VerificationTxState.OnKeyReceived  -> {
                     setState {
                         copy(
                                 isWaitingFromOther = false,
    @@ -85,7 +86,7 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
                         )
                     }
                 }
    -            SasVerificationTxState.ShortCodeReady -> {
    +            VerificationTxState.ShortCodeReady -> {
                     setState {
                         copy(
                                 isWaitingFromOther = false,
    @@ -97,17 +98,17 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
                         )
                     }
                 }
    -            SasVerificationTxState.ShortCodeAccepted,
    -            SasVerificationTxState.SendingMac,
    -            SasVerificationTxState.MacSent,
    -            SasVerificationTxState.Verifying,
    -            SasVerificationTxState.Verified       -> {
    +            VerificationTxState.ShortCodeAccepted,
    +            VerificationTxState.SendingMac,
    +            VerificationTxState.MacSent,
    +            VerificationTxState.Verifying,
    +            VerificationTxState.Verified       -> {
                     setState {
                         copy(isWaitingFromOther = true)
                     }
                 }
    -            SasVerificationTxState.Cancelled,
    -            SasVerificationTxState.OnCancelled    -> {
    +            VerificationTxState.Cancelled,
    +            VerificationTxState.OnCancelled    -> {
                     // The fragment should not be rendered in this state,
                     // it should have been replaced by a conclusion fragment
                     setState {
    @@ -131,12 +132,12 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
             }
         }
     
    -    override fun transactionCreated(tx: SasVerificationTransaction) {
    +    override fun transactionCreated(tx: VerificationTransaction) {
             transactionUpdated(tx)
         }
     
    -    override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
    -        if (tx.transactionId == state.transactionId) {
    +    override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    +        if (tx.transactionId == state.transactionId && tx is SasVerificationTransaction) {
                 refreshStateFromTx(tx)
             }
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt
    new file mode 100644
    index 0000000000..dc126bc460
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationQrCodeItem.kt
    @@ -0,0 +1,45 @@
    +/*
    + * Copyright 2020 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.riotx.features.crypto.verification.epoxy
    +
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.ui.views.QrCodeImageView
    +
    +/**
    + * An Epoxy item displaying a QR code
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_qr_code)
    +abstract class BottomSheetVerificationQrCodeItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    lateinit var data: String
    +
    +    @EpoxyAttribute
    +    var animate = false
    +
    +    override fun bind(holder: Holder) {
    +        holder.qsrCodeImage.setData(data, animate)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val qsrCodeImage by bind(R.id.itemVerificationQrCodeImage)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    index e14b6573f4..3bc960e676 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    @@ -19,11 +19,14 @@ package im.vector.riotx.features.crypto.verification.request
     import androidx.core.text.toSpannable
     import com.airbnb.epoxy.EpoxyController
     import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.dividerItem
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.utils.colorizeMatchingText
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
     import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
     import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
     import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
    @@ -36,17 +39,18 @@ class VerificationRequestController @Inject constructor(
     
         var listener: Listener? = null
     
    -    private var viewState: VerificationRequestViewState? = null
    +    private var viewState: VerificationBottomSheetViewState? = null
     
    -    fun update(viewState: VerificationRequestViewState) {
    +    fun update(viewState: VerificationBottomSheetViewState) {
             this.viewState = viewState
             requestModelBuild()
         }
     
         override fun buildModels() {
             val state = viewState ?: return
    +        val matrixItem = viewState?.otherUserMxItem ?: return
     
    -        val styledText = state.matrixItem.let {
    +        val styledText = matrixItem.let {
                 stringProvider.getString(R.string.verification_request_notice, it.id)
                         .toSpannable()
                         .colorizeMatchingText(it.id, colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color))
    @@ -61,14 +65,8 @@ class VerificationRequestController @Inject constructor(
                 id("sep")
             }
     
    -        when (state.started) {
    -            is Loading -> {
    -                bottomSheetVerificationWaitingItem {
    -                    id("waiting")
    -                    title(stringProvider.getString(R.string.verification_request_waiting_for, state.matrixItem.getBestName()))
    -                }
    -            }
    -            else       -> {
    +        when (val pr = state.pendingRequest) {
    +            is Uninitialized -> {
                     bottomSheetVerificationActionItem {
                         id("start")
                         title(stringProvider.getString(R.string.start_verification))
    @@ -79,6 +77,20 @@ class VerificationRequestController @Inject constructor(
                         listener { listener?.onClickOnVerificationStart() }
                     }
                 }
    +            is Loading -> {
    +                bottomSheetVerificationWaitingItem {
    +                    id("waiting")
    +                    title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
    +                }
    +            }
    +            is Success -> {
    +                if (!pr.invoke().isReady) {
    +                    bottomSheetVerificationWaitingItem {
    +                        id("waiting")
    +                        title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
    +                    }
    +                }
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    index 8250bd74b8..8231242d08 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    @@ -17,7 +17,6 @@ package im.vector.riotx.features.crypto.verification.request
     
     import android.os.Bundle
     import android.view.View
    -import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.parentFragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.riotx.R
    @@ -30,19 +29,15 @@ import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
     import javax.inject.Inject
     
     class VerificationRequestFragment @Inject constructor(
    -        val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
             val controller: VerificationRequestController
     ) : VectorBaseFragment(), VerificationRequestController.Listener {
     
    -    private val viewModel by fragmentViewModel(VerificationRequestViewModel::class)
    -
    -    private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
    +    private val viewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
     
         override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
     
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
    -
             setupRecyclerView()
         }
     
    @@ -61,7 +56,9 @@ class VerificationRequestFragment @Inject constructor(
             controller.update(state)
         }
     
    -    override fun onClickOnVerificationStart() = withState(viewModel) { state ->
    -        sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
    +    override fun onClickOnVerificationStart(): Unit = withState(viewModel) { state ->
    +        state.otherUserMxItem?.id?.let { otherUserId ->
    +            viewModel.handle(VerificationAction.RequestVerificationByDM(otherUserId, state.roomId))
    +        }
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestViewModel.kt
    deleted file mode 100644
    index 3984853716..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestViewModel.kt
    +++ /dev/null
    @@ -1,104 +0,0 @@
    -/*
    - * Copyright 2019 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.riotx.features.crypto.verification.request
    -
    -import com.airbnb.mvrx.*
    -import com.squareup.inject.assisted.Assisted
    -import com.squareup.inject.assisted.AssistedInject
    -import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    -import im.vector.matrix.android.api.util.MatrixItem
    -import im.vector.matrix.android.api.util.toMatrixItem
    -import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
    -import im.vector.riotx.core.di.HasScreenInjector
    -import im.vector.riotx.core.platform.EmptyAction
    -import im.vector.riotx.core.platform.EmptyViewEvents
    -import im.vector.riotx.core.platform.VectorViewModel
    -import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
    -
    -data class VerificationRequestViewState(
    -        val roomId: String? = null,
    -        val matrixItem: MatrixItem,
    -        val started: Async = Success(false)
    -) : MvRxState
    -
    -class VerificationRequestViewModel @AssistedInject constructor(
    -        @Assisted initialState: VerificationRequestViewState,
    -        private val session: Session
    -) : VectorViewModel(initialState), SasVerificationService.SasVerificationListener {
    -
    -    @AssistedInject.Factory
    -    interface Factory {
    -        fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
    -    }
    -
    -    init {
    -        session.getSasVerificationService().addListener(this)
    -    }
    -
    -    override fun onCleared() {
    -        session.getSasVerificationService().removeListener(this)
    -        super.onCleared()
    -    }
    -
    -    companion object : MvRxViewModelFactory {
    -        override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? {
    -            val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
    -            return fragment.verificationRequestViewModelFactory.create(state)
    -        }
    -
    -        override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
    -            val args = viewModelContext.args()
    -            val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    -
    -            val pr = session.getSasVerificationService()
    -                    .getExistingVerificationRequest(args.otherUserId, args.verificationId)
    -            return session.getUser(args.otherUserId)?.let {
    -                VerificationRequestViewState(
    -                        started = Success(false).takeIf { pr == null }
    -                                ?: Success(true).takeIf { pr?.isReady == true }
    -                                ?: Loading(),
    -                        matrixItem = it.toMatrixItem()
    -                )
    -            }
    -        }
    -    }
    -
    -    override fun handle(action: EmptyAction) {}
    -
    -    override fun transactionCreated(tx: SasVerificationTransaction) {}
    -
    -    override fun transactionUpdated(tx: SasVerificationTransaction) {}
    -
    -    override fun verificationRequestCreated(pr: PendingVerificationRequest) {
    -        verificationRequestUpdated(pr)
    -    }
    -
    -    override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
    -        if (pr.otherUserId == state.matrixItem.id) {
    -            if (pr.isReady) {
    -                setState {
    -                    copy(started = Success(true))
    -                }
    -            } else {
    -                setState {
    -                    copy(started = Loading())
    -                }
    -            }
    -        }
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    index 0aea65ec15..e51c25b0e9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
    @@ -1024,6 +1024,7 @@ class RoomDetailFragment @Inject constructor(
         }
     
         override fun onAvatarClicked(informationData: MessageInformationData) {
    +        // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
             openRoomMemberProfile(informationData.senderId)
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    index 4d534341d2..f6a4717c78 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
    @@ -188,7 +188,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is RoomDetailAction.AcceptVerificationRequest        -> handleAcceptVerification(action)
                 is RoomDetailAction.DeclineVerificationRequest       -> handleDeclineVerification(action)
                 is RoomDetailAction.RequestVerification              -> handleRequestVerification(action)
    -            is RoomDetailAction.ResumeVerification              -> handleResumeRequestVerification(action)
    +            is RoomDetailAction.ResumeVerification               -> handleResumeRequestVerification(action)
             }
         }
     
    @@ -411,7 +411,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                                 popDraft()
                             }
                             is ParsedCommand.VerifyUser               -> {
    -                            session.getSasVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
    +                            session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
                                 _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
                                 popDraft()
                             }
    @@ -809,14 +809,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     
         private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
             Timber.v("## SAS handleAcceptVerification ${action.otherUserId},  roomId:${room.roomId}, txId:${action.transactionId}")
    -        if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
    -                action.transactionId)) {
    +        if (session.getVerificationService().readyPendingVerificationInDMs(
    +                        supportedVerificationMethods,
    +                        action.otherUserId,
    +                        room.roomId,
    +                        action.transactionId)) {
                 _requestLiveData.postValue(LiveEvent(Success(action)))
    +        } else {
    +            // TODO
             }
         }
     
         private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
    -        session.getSasVerificationService().declineVerificationRequestInDMs(
    +        session.getVerificationService().declineVerificationRequestInDMs(
                     action.otherUserId,
                     action.otherdDeviceId,
                     action.transactionId,
    @@ -824,12 +829,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
    +        if (action.userId == session.myUserId) return
             _requestLiveData.postValue(LiveEvent(Success(action)))
         }
     
         private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
             // Check if this request is still active and handled by me
    -        session.getSasVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
    +        session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
                 if (it.handledByOtherSession) return
                 if (!it.isFinished) {
                     _requestLiveData.postValue(LiveEvent(Success(action.copy(
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    index 4704a3de85..1462f5fe0d 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
    @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.riotx.core.epoxy.EmptyItem_
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.resources.UserPreferencesProvider
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
     import timber.log.Timber
     import javax.inject.Inject
    @@ -29,7 +30,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                                                   private val noticeItemFactory: NoticeItemFactory,
                                                   private val defaultItemFactory: DefaultItemFactory,
                                                   private val roomCreateItemFactory: RoomCreateItemFactory,
    -                                              private val verificationConclusionItemFactory: VerificationItemFactory) {
    +                                              private val verificationConclusionItemFactory: VerificationItemFactory,
    +                                              private val userPreferencesProvider: UserPreferencesProvider) {
     
         fun create(event: TimelineEvent,
                    nextEvent: TimelineEvent?,
    @@ -73,9 +75,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                     EventType.KEY_VERIFICATION_KEY,
                     EventType.KEY_VERIFICATION_READY,
                     EventType.KEY_VERIFICATION_MAC          -> {
    -                    // These events are filtered from timeline in normal case
    -                    // Only visible in developer mode
    -                    noticeItemFactory.create(event, highlight, callback)
    +                    // TODO These are not filtered out by timeline when encrypted
    +                    // For now manually ignore
    +                    if (userPreferencesProvider.shouldShowHiddenEvents()) {
    +                        noticeItemFactory.create(event, highlight, callback)
    +                    } else null
                     }
                     EventType.KEY_VERIFICATION_CANCEL,
                     EventType.KEY_VERIFICATION_DONE         -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    index 5e24c69ad8..dc5bd740dd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    @@ -108,7 +108,7 @@ class VerificationItemFactory @Inject constructor(
                                     .highlighted(highlight)
                                     .leftGuideline(avatarSizeProvider.leftGuideline)
                         }
    -                    else                     -> ignoredConclusion(event, highlight, callback)
    +                    else                     -> return ignoredConclusion(event, highlight, callback)
                     }
                 }
                 EventType.KEY_VERIFICATION_DONE   -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    index a0ef876f1a..24f992a001 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    @@ -28,7 +28,7 @@ import androidx.core.view.isVisible
     import androidx.core.view.updateLayoutParams
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
     import im.vector.matrix.android.internal.session.room.VerificationState
     import im.vector.riotx.R
     import im.vector.riotx.core.resources.ColorProvider
    @@ -110,7 +110,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem next.expirationTimestamp!!) {
                     // skip
    diff --git a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    new file mode 100644
    index 0000000000..bb6f1be03b
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    @@ -0,0 +1,78 @@
    +/*
    + * Copyright 2020 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.riotx.features.qrcode
    +
    +import android.app.Activity
    +import android.content.Intent
    +import android.os.Bundle
    +import androidx.fragment.app.Fragment
    +import com.google.zxing.BarcodeFormat
    +import com.google.zxing.Result
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.extensions.replaceFragment
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +
    +class QrCodeScannerActivity : VectorBaseActivity() {
    +
    +    override fun getLayoutRes() = R.layout.activity_simple
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +        if (isFirstCreation()) {
    +            replaceFragment(R.id.simpleFragmentContainer, QrCodeScannerFragment::class.java)
    +        }
    +    }
    +
    +    fun setResultAndFinish(result: Result?) {
    +        result?.let {
    +            setResult(RESULT_OK, Intent().apply {
    +                putExtra(EXTRA_OUT_TEXT, it.text)
    +                putExtra(EXTRA_OUT_IS_QR_CODE, it.barcodeFormat == BarcodeFormat.QR_CODE)
    +            })
    +        }
    +        finish()
    +    }
    +
    +    companion object {
    +        private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
    +        private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE"
    +
    +        const val QR_CODE_SCANNER_REQUEST_CODE = 429
    +
    +        // For test only
    +        fun startForResult(activity: Activity, requestCode: Int = QR_CODE_SCANNER_REQUEST_CODE) {
    +            activity.startActivityForResult(Intent(activity, QrCodeScannerActivity::class.java), requestCode)
    +        }
    +
    +        fun startForResult(fragment: Fragment, requestCode: Int = QR_CODE_SCANNER_REQUEST_CODE) {
    +            fragment.startActivityForResult(Intent(fragment.requireActivity(), QrCodeScannerActivity::class.java), requestCode)
    +        }
    +
    +        fun getResultText(data: Intent?): String? {
    +            return data?.getStringExtra(EXTRA_OUT_TEXT)
    +        }
    +
    +        fun getResultIsQrCode(data: Intent?): Boolean {
    +            return data?.getBooleanExtra(EXTRA_OUT_IS_QR_CODE, false) == true
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt
    new file mode 100644
    index 0000000000..2c6e9ed3d5
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt
    @@ -0,0 +1,51 @@
    +/*
    + * Copyright 2020 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.riotx.features.qrcode
    +
    +import com.google.zxing.Result
    +import im.vector.riotx.R
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
    +import me.dm7.barcodescanner.zxing.ZXingScannerView
    +import javax.inject.Inject
    +
    +class QrCodeScannerFragment @Inject constructor()
    +    : VectorBaseFragment(),
    +        ZXingScannerView.ResultHandler {
    +
    +    override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
    +
    +    override fun onResume() {
    +        super.onResume()
    +        // Register ourselves as a handler for scan results.
    +        scannerView.setResultHandler(this)
    +        // Start camera on resume
    +        scannerView.startCamera()
    +    }
    +
    +    override fun onPause() {
    +        super.onPause()
    +        // Stop camera on pause
    +        scannerView.stopCamera()
    +    }
    +
    +    override fun handleResult(rawResult: Result?) {
    +        // Do something with the result here
    +        // This is not intended to be used outside of QrCodeScannerActivity for the moment
    +        (requireActivity() as? QrCodeScannerActivity)?.setResultAndFinish(rawResult)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    index 8ff209b443..9122b180e8 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt
    @@ -23,4 +23,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
     
         object RetryFetchingInfo: RoomMemberProfileAction()
         object IgnoreUser: RoomMemberProfileAction()
    +    data class VerifyUser(val userId: String? = null, val roomId: String? = null): RoomMemberProfileAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt
    index 23c713068a..5a18588daa 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt
    @@ -23,6 +23,7 @@ import im.vector.riotx.core.epoxy.profiles.buildProfileAction
     import im.vector.riotx.core.epoxy.profiles.buildProfileSection
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.genericFooterItem
     import javax.inject.Inject
     
     class RoomMemberProfileController @Inject constructor(
    @@ -36,7 +37,9 @@ class RoomMemberProfileController @Inject constructor(
     
         interface Callback {
             fun onIgnoreClicked()
    -        fun onLearnMoreClicked()
    +        fun onTapVerify()
    +        fun onShowDeviceList()
    +        fun onShowDeviceListNoCrossSigning()
             fun onJumpToReadReceiptClicked()
             fun onMentionClicked()
         }
    @@ -70,20 +73,65 @@ class RoomMemberProfileController @Inject constructor(
         private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {
             // Security
             buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
    -        val learnMoreSubtitle = if (state.isRoomEncrypted) {
    -            R.string.room_profile_encrypted_subtitle
    +
    +        if (state.isRoomEncrypted) {
    +            if (state.userMXCrossSigningInfo != null) {
    +                // Cross signing is enabled for this user
    +                if (state.userMXCrossSigningInfo.isTrusted()) {
    +                    // User is trusted
    +                    val icon = if (state.allDevicesAreTrusted.invoke() == true) R.drawable.ic_shield_trusted
    +                    else R.drawable.ic_shield_warning
    +
    +                    val titleRes = if (state.allDevicesAreTrusted.invoke() == true) R.string.verification_profile_verified
    +                    else R.string.verification_profile_warning
    +
    +                    buildProfileAction(
    +                            id = "learn_more",
    +                            title = stringProvider.getString(titleRes),
    +                            dividerColor = dividerColor,
    +                            editable = true,
    +                            icon = icon,
    +                            divider = false,
    +                            action = { callback?.onShowDeviceList() }
    +                    )
    +                } else {
    +                    // Not trusted, propose to verify
    +                    if (!state.isMine) {
    +                        buildProfileAction(
    +                                id = "learn_more",
    +                                title = stringProvider.getString(R.string.verification_profile_verify),
    +                                dividerColor = dividerColor,
    +                                editable = true,
    +                                icon = R.drawable.ic_shield_black,
    +                                divider = false,
    +                                action = { callback?.onTapVerify() }
    +                        )
    +                    }
    +
    +                    genericFooterItem {
    +                        id("verify_footer")
    +                        text(stringProvider.getString(R.string.room_profile_encrypted_subtitle))
    +                        centered(false)
    +                    }
    +                }
    +            } else {
    +                buildProfileAction(
    +                        id = "learn_more",
    +                        title = stringProvider.getString(R.string.room_profile_section_security_learn_more),
    +                        dividerColor = dividerColor,
    +                        editable = false,
    +                        divider = false,
    +                        subtitle = stringProvider.getString(R.string.room_profile_encrypted_subtitle),
    +                        action = { callback?.onShowDeviceListNoCrossSigning() }
    +                )
    +            }
             } else {
    -            R.string.room_profile_not_encrypted_subtitle
    +            genericFooterItem {
    +                id("verify_footer_not_encrypted")
    +                text(stringProvider.getString(R.string.room_profile_not_encrypted_subtitle))
    +                centered(false)
    +            }
             }
    -        buildProfileAction(
    -                id = "learn_more",
    -                title = stringProvider.getString(R.string.room_profile_section_security_learn_more),
    -                dividerColor = dividerColor,
    -                editable = false,
    -                divider = false,
    -                subtitle = stringProvider.getString(learnMoreSubtitle),
    -                action = { callback?.onLearnMoreClicked() }
    -        )
     
             // More
             if (!state.isMine) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    index 3d1455042c..5a711f5cdb 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt
    @@ -33,10 +33,13 @@ import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.extensions.exhaustive
    +import im.vector.riotx.core.extensions.observeEvent
     import im.vector.riotx.core.extensions.setTextOrHide
     import im.vector.riotx.core.platform.StateView
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
     import kotlinx.android.parcel.Parcelize
     import kotlinx.android.synthetic.main.fragment_matrix_profile.*
     import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.*
    @@ -86,6 +89,19 @@ class RoomMemberProfileFragment @Inject constructor(
                     is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
                 }.exhaustive
             }
    +        viewModel.actionResultLiveData.observeEvent(this) { async ->
    +            when (async) {
    +                is Success -> {
    +                    when (val action = async.invoke()) {
    +                        is RoomMemberProfileAction.VerifyUser -> {
    +                            VerificationBottomSheet
    +                                    .withArgs(roomId = null, otherUserId = action.userId!!)
    +                                    .show(parentFragmentManager, "VERIF")
    +                        }
    +                    }
    +                }
    +            }
    +        }
         }
     
         override fun onDestroyView() {
    @@ -129,8 +145,16 @@ class RoomMemberProfileFragment @Inject constructor(
             viewModel.handle(RoomMemberProfileAction.IgnoreUser)
         }
     
    -    override fun onLearnMoreClicked() {
    -        vectorBaseActivity.notImplemented("Learn more")
    +    override fun onTapVerify() {
    +        viewModel.handle(RoomMemberProfileAction.VerifyUser())
    +    }
    +
    +    override fun onShowDeviceList() = withState(viewModel) {
    +        DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
    +    }
    +
    +    override fun onShowDeviceListNoCrossSigning() = withState(viewModel) {
    +        DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
         }
     
         override fun onJumpToReadReceiptClicked() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    index a2488b1ed4..aba6274dff 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt
    @@ -17,9 +17,13 @@
     
     package im.vector.riotx.features.roommemberprofile
     
    +import androidx.lifecycle.LiveData
    +import androidx.lifecycle.MutableLiveData
     import androidx.lifecycle.viewModelScope
    +import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.FragmentViewModelContext
     import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
    @@ -44,6 +48,7 @@ import im.vector.matrix.rx.unwrap
     import im.vector.riotx.R
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.utils.LiveEvent
     import io.reactivex.Observable
     import io.reactivex.functions.BiFunction
     import kotlinx.coroutines.Dispatchers
    @@ -69,6 +74,10 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
             }
         }
     
    +    private val _actionResultLiveData = MutableLiveData>>()
    +    val actionResultLiveData: LiveData>>
    +        get() = _actionResultLiveData
    +
         private val room = if (initialState.roomId != null) {
             session.getRoom(initialState.roomId)
         } else {
    @@ -92,6 +101,19 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
                     observeRoomMemberSummary(room)
                     observeRoomSummaryAndPowerLevels(room)
                 }
    +
    +            session.rx().liveUserCryptoDevices(initialState.userId)
    +                    .map {
    +                        it.fold(true, { prev, dev -> prev && dev.isVerified })
    +                    }
    +                    .execute {
    +                        copy(allDevicesAreTrusted = it)
    +                    }
    +
    +            session.rx().liveCrossSigningInfo(initialState.userId)
    +                    .execute {
    +                        copy(userMXCrossSigningInfo = it.invoke()?.getOrNull())
    +                    }
             }
         }
     
    @@ -109,8 +131,21 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
     
         override fun handle(action: RoomMemberProfileAction) {
             when (action) {
    -            RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
    -            is RoomMemberProfileAction.IgnoreUser     -> handleIgnoreAction()
    +            is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
    +            is RoomMemberProfileAction.IgnoreUser        -> handleIgnoreAction()
    +            is RoomMemberProfileAction.VerifyUser        -> prepareVerification(action)
    +        }
    +    }
    +
    +    private fun prepareVerification(action: RoomMemberProfileAction.VerifyUser) = withState { state ->
    +        // Sanity
    +        if (state.isRoomEncrypted) {
    +            if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) {
    +                // ok, let's find or create the DM room
    +                _actionResultLiveData.postValue(
    +                        LiveEvent(Success(action.copy(userId = state.userId)))
    +                )
    +            }
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt
    index ba079e9996..b9044b1d36 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt
    @@ -20,6 +20,7 @@ package im.vector.riotx.features.roommemberprofile
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.MvRxState
     import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
     import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
     import im.vector.matrix.android.api.util.MatrixItem
     
    @@ -32,7 +33,9 @@ data class RoomMemberProfileViewState(
             val isRoomEncrypted: Boolean = false,
             val powerLevelsContent: Async = Uninitialized,
             val userPowerLevelString: Async = Uninitialized,
    -        val userMatrixItem: Async = Uninitialized
    +        val userMatrixItem: Async = Uninitialized,
    +        val userMXCrossSigningInfo: MXCrossSigningInfo? = null,
    +        val allDevicesAreTrusted: Async = Uninitialized
     ) : MvRxState {
     
         constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt
    new file mode 100644
    index 0000000000..dc8ed66fb8
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheet.kt
    @@ -0,0 +1,120 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import android.content.DialogInterface
    +import android.os.Bundle
    +import android.view.KeyEvent
    +import androidx.fragment.app.Fragment
    +import com.airbnb.mvrx.MvRx
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.extensions.commitTransaction
    +import im.vector.riotx.core.extensions.observeEvent
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.features.crypto.verification.VerificationAction
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
    +import javax.inject.Inject
    +import kotlin.reflect.KClass
    +
    +class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_with_fragments
    +
    +    private val viewModel: DeviceListBottomSheetViewModel by fragmentViewModel(DeviceListBottomSheetViewModel::class)
    +
    +    @Inject lateinit var viewModelFactory: DeviceListBottomSheetViewModel.Factory
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        viewModel.requestLiveData.observeEvent(this) { async ->
    +            when (async) {
    +                is Success -> {
    +                    when (val action = async.invoke()) {
    +                        is VerificationAction.StartSASVerification -> {
    +                            VerificationBottomSheet.withArgs(
    +                                    roomId = null,
    +                                    otherUserId = action.otherUserId,
    +                                    transactionId = action.pendingRequestTransactionId
    +                            ).show(requireActivity().supportFragmentManager, "REQPOP")
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private val onKeyListener = DialogInterface.OnKeyListener { _, keyCode, _ ->
    +        withState(viewModel) {
    +            if (keyCode == KeyEvent.KEYCODE_BACK) {
    +                if (it.selectedDevice != null) {
    +                    viewModel.selectDevice(null)
    +                    return@withState true
    +                } else {
    +                    return@withState false
    +                }
    +            }
    +            return@withState false
    +        }
    +    }
    +
    +    override fun onResume() {
    +        super.onResume()
    +        dialog?.setOnKeyListener(onKeyListener)
    +    }
    +
    +    override fun onPause() {
    +        super.onPause()
    +        dialog?.setOnKeyListener(null)
    +    }
    +
    +    override fun invalidate() = withState(viewModel) {
    +        super.invalidate()
    +        if (it.selectedDevice == null) {
    +            showFragment(DeviceListFragment::class, arguments ?: Bundle())
    +        } else {
    +            showFragment(DeviceTrustInfoActionFragment::class, arguments ?: Bundle())
    +        }
    +    }
    +
    +    private fun showFragment(fragmentClass: KClass, bundle: Bundle) {
    +        if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
    +            childFragmentManager.commitTransaction {
    +                replace(R.id.bottomSheetFragmentContainer,
    +                        fragmentClass.java,
    +                        bundle,
    +                        fragmentClass.simpleName
    +                )
    +            }
    +        }
    +    }
    +
    +    companion object {
    +        fun newInstance(userId: String): DeviceListBottomSheet {
    +            val args = Bundle()
    +            args.putString(MvRx.KEY_ARG, userId)
    +            return DeviceListBottomSheet().apply { arguments = args }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    new file mode 100644
    index 0000000000..48e8c38dec
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt
    @@ -0,0 +1,129 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import androidx.lifecycle.LiveData
    +import androidx.lifecycle.MutableLiveData
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.util.MatrixItem
    +import im.vector.matrix.android.api.util.toMatrixItem
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.di.HasScreenInjector
    +import im.vector.riotx.core.platform.EmptyAction
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.utils.LiveEvent
    +import im.vector.riotx.features.crypto.verification.VerificationAction
    +
    +data class DeviceListViewState(
    +        val userItem: MatrixItem? = null,
    +        val isMine: Boolean = false,
    +        val memberCrossSigningKey: MXCrossSigningInfo? = null,
    +        val cryptoDevices: Async> = Loading(),
    +        val selectedDevice: CryptoDeviceInfo? = null
    +) : MvRxState
    +
    +class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
    +                                                                 @Assisted private val userId: String,
    +                                                                 private val stringProvider: StringProvider,
    +                                                                 private val session: Session)
    +    : VectorViewModel(initialState) {
    +
    +    // Can be used for several actions, for a one shot result
    +    private val _requestLiveData = MutableLiveData>>()
    +    val requestLiveData: LiveData>>
    +        get() = _requestLiveData
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel
    +    }
    +
    +    init {
    +
    +        session.rx().liveUserCryptoDevices(userId)
    +                .execute {
    +                    copy(cryptoDevices = it).also {
    +                        refreshSelectedId()
    +                    }
    +                }
    +
    +        session.rx().liveCrossSigningInfo(userId)
    +                .execute {
    +                    copy(memberCrossSigningKey = it.invoke()?.getOrNull())
    +                }
    +    }
    +
    +    private fun refreshSelectedId() = withState { state ->
    +        if (state.selectedDevice != null) {
    +            state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let {
    +                setState {
    +                    copy(
    +                            selectedDevice = it
    +                    )
    +                }
    +            }
    +        }
    +    }
    +
    +    fun selectDevice(device: CryptoDeviceInfo?) {
    +        setState {
    +            copy(selectedDevice = device)
    +        }
    +    }
    +
    +    fun manuallyVerify(device: CryptoDeviceInfo) {
    +        session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId)?.let { txID ->
    +            _requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
    +        }
    +    }
    +
    +    override fun handle(action: EmptyAction) {}
    +
    +    companion object : MvRxViewModelFactory {
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? {
    +            val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            val userId = viewModelContext.args()
    +            return fragment.viewModelFactory.create(state, userId)
    +        }
    +
    +        override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
    +            val userId = viewModelContext.args()
    +            val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    +            return session.getUser(userId)?.toMatrixItem()?.let {
    +                DeviceListViewState(
    +                        userItem = it,
    +                        isMine = userId == session.myUserId
    +                )
    +            } ?: return super.initialState(viewModelContext)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt
    new file mode 100644
    index 0000000000..50a87b24a4
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListEpoxyController.kt
    @@ -0,0 +1,204 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import android.view.View
    +import com.airbnb.epoxy.TypedEpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.errorWithRetryItem
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.GenericItem
    +import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.core.ui.list.genericItem
    +import im.vector.riotx.core.ui.list.genericItemWithValue
    +import im.vector.riotx.core.utils.DimensionConverter
    +import im.vector.riotx.features.settings.VectorPreferences
    +import me.gujun.android.span.span
    +import javax.inject.Inject
    +
    +class DeviceListEpoxyController @Inject constructor(private val stringProvider: StringProvider,
    +                                                    private val colorProvider: ColorProvider,
    +                                                    private val dimensionConverter: DimensionConverter,
    +                                                    private val vectorPreferences: VectorPreferences)
    +    : TypedEpoxyController() {
    +
    +    interface InteractionListener {
    +        fun onDeviceSelected(device: CryptoDeviceInfo)
    +    }
    +
    +    var interactionListener: InteractionListener? = null
    +
    +    override fun buildModels(data: DeviceListViewState?) {
    +        if (data == null) {
    +            return
    +        }
    +        when (data.cryptoDevices) {
    +            Uninitialized -> {
    +            }
    +            is Loading    -> {
    +                loadingItem {
    +                    id("loading")
    +                    loadingText(stringProvider.getString(R.string.loading))
    +                }
    +            }
    +            is Success    -> {
    +                val deviceList = data.cryptoDevices.invoke().sortedByDescending {
    +                    it.isVerified
    +                }
    +
    +                // Build top header
    +                val allGreen = deviceList.fold(true, { prev, device ->
    +                    prev && device.isVerified
    +                })
    +
    +                genericItem {
    +                    id("title")
    +                    style(GenericItem.STYLE.BIG_TEXT)
    +                    titleIconResourceId(if (allGreen) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
    +                    title(
    +                            stringProvider.getString(
    +                                    if (allGreen) R.string.verification_profile_verified else R.string.verification_profile_warning
    +                            )
    +                    )
    +                    description(stringProvider.getString(R.string.verification_conclusion_ok_notice))
    +                }
    +
    +                if (vectorPreferences.developerMode()) {
    +                    // Display the cross signing keys
    +                    addDebugInfo(data)
    +                }
    +
    +                genericItem {
    +                    id("sessions")
    +                    style(GenericItem.STYLE.BIG_TEXT)
    +                    title(stringProvider.getString(R.string.room_member_profile_sessions_section_title))
    +                }
    +                if (deviceList.isEmpty()) {
    +                    // Can this really happen?
    +                    genericFooterItem {
    +                        id("empty")
    +                        text(stringProvider.getString(R.string.search_no_results))
    +                    }
    +                } else {
    +                    // Build list of device with status
    +                    deviceList.forEach { device ->
    +                        genericItemWithValue {
    +                            id(device.deviceId)
    +                            titleIconResourceId(if (device.isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
    +                            apply {
    +                                if (vectorPreferences.developerMode()) {
    +                                    val seq = span {
    +                                        +(device.displayName() ?: device.deviceId)
    +                                        +"\n"
    +                                        span {
    +                                            text = "(${device.deviceId})"
    +                                            textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                            textSize = dimensionConverter.spToPx(14)
    +                                        }
    +                                    }
    +                                    title(seq)
    +                                } else {
    +                                    title(device.displayName() ?: device.deviceId)
    +                                }
    +                            }
    +                            value(
    +                                    stringProvider.getString(
    +                                            if (device.isVerified) R.string.trusted else R.string.not_trusted
    +                                    )
    +                            )
    +                            valueColorInt(
    +                                    colorProvider.getColor(
    +                                            if (device.isVerified) R.color.riotx_positive_accent else R.color.riotx_destructive_accent
    +                                    )
    +                            )
    +                            itemClickAction(View.OnClickListener {
    +                                interactionListener?.onDeviceSelected(device)
    +                            })
    +                        }
    +                    }
    +                }
    +            }
    +            is Fail       -> {
    +                errorWithRetryItem {
    +                    id("error")
    +                    text(stringProvider.getString(R.string.room_member_profile_failed_to_get_devices))
    +                    listener {
    +                        // TODO
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun addDebugInfo(data: DeviceListViewState) {
    +        data.memberCrossSigningKey?.masterKey()?.let {
    +            genericItemWithValue {
    +                id("msk")
    +                titleIconResourceId(R.drawable.key_small)
    +                title(
    +                        span {
    +                            +"Master Key:\n"
    +                            span {
    +                                text = it.unpaddedBase64PublicKey ?: ""
    +                                textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                textSize = dimensionConverter.spToPx(12)
    +                            }
    +                        }
    +                )
    +            }
    +        }
    +        data.memberCrossSigningKey?.userKey()?.let {
    +            genericItemWithValue {
    +                id("usk")
    +                titleIconResourceId(R.drawable.key_small)
    +                title(
    +                        span {
    +                            +"User Key:\n"
    +                            span {
    +                                text = it.unpaddedBase64PublicKey ?: ""
    +                                textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                textSize = dimensionConverter.spToPx(12)
    +                            }
    +                        }
    +                )
    +            }
    +        }
    +        data.memberCrossSigningKey?.selfSigningKey()?.let {
    +            genericItemWithValue {
    +                id("ssk")
    +                titleIconResourceId(R.drawable.key_small)
    +                title(
    +                        span {
    +                            +"Self Signed Key:\n"
    +                            span {
    +                                text = it.unpaddedBase64PublicKey ?: ""
    +                                textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                textSize = dimensionConverter.spToPx(12)
    +                            }
    +                        }
    +                )
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt
    new file mode 100644
    index 0000000000..c598d051f7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceListFragment.kt
    @@ -0,0 +1,67 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import android.os.Bundle
    +import androidx.recyclerview.widget.RecyclerView
    +import butterknife.BindView
    +import com.airbnb.mvrx.parentFragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.DimensionConverter
    +import javax.inject.Inject
    +
    +class DeviceListFragment @Inject constructor(
    +        val dimensionConverter: DimensionConverter,
    +        val epoxyController: DeviceListEpoxyController
    +) : VectorBaseFragment(), DeviceListEpoxyController.InteractionListener {
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
    +
    +    private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
    +
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        recyclerView.setPadding(0, dimensionConverter.dpToPx(16), 0, dimensionConverter.dpToPx(16))
    +        recyclerView.configureWith(
    +                epoxyController,
    +                showDivider = false,
    +                hasFixedSize = false)
    +        epoxyController.interactionListener = this
    +    }
    +
    +    override fun onDestroyView() {
    +        recyclerView.cleanup()
    +        super.onDestroyView()
    +    }
    +
    +    override fun invalidate() = withState(viewModel) {
    +        epoxyController.setData(it)
    +        super.invalidate()
    +    }
    +
    +    override fun onDeviceSelected(device: CryptoDeviceInfo) {
    +        viewModel.selectDevice(device)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt
    new file mode 100644
    index 0000000000..6c9de742bc
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoActionFragment.kt
    @@ -0,0 +1,67 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import android.os.Bundle
    +import androidx.recyclerview.widget.RecyclerView
    +import butterknife.BindView
    +import com.airbnb.mvrx.parentFragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.DimensionConverter
    +import javax.inject.Inject
    +
    +class DeviceTrustInfoActionFragment @Inject constructor(
    +        val dimensionConverter: DimensionConverter,
    +        val epoxyController: DeviceTrustInfoEpoxyController
    +) : VectorBaseFragment(), DeviceTrustInfoEpoxyController.InteractionListener {
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
    +
    +    private val viewModel: DeviceListBottomSheetViewModel by parentFragmentViewModel(DeviceListBottomSheetViewModel::class)
    +
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        recyclerView.setPadding(0, dimensionConverter.dpToPx(16), 0, dimensionConverter.dpToPx(16))
    +        recyclerView.configureWith(
    +                epoxyController,
    +                showDivider = false,
    +                hasFixedSize = false)
    +        epoxyController.interactionListener = this
    +    }
    +
    +    override fun onDestroyView() {
    +        recyclerView.cleanup()
    +        super.onDestroyView()
    +    }
    +
    +    override fun invalidate() = withState(viewModel) {
    +        epoxyController.setData(it)
    +        super.invalidate()
    +    }
    +
    +    override fun onVerifyManually(device: CryptoDeviceInfo) {
    +        viewModel.manuallyVerify(device)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt
    new file mode 100644
    index 0000000000..907c019f39
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/devices/DeviceTrustInfoEpoxyController.kt
    @@ -0,0 +1,115 @@
    +/*
    + * Copyright 2020 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.riotx.features.roommemberprofile.devices
    +
    +import com.airbnb.epoxy.TypedEpoxyController
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.GenericItem
    +import im.vector.riotx.core.ui.list.genericFooterItem
    +import im.vector.riotx.core.ui.list.genericItem
    +import im.vector.riotx.core.ui.list.genericItemWithValue
    +import im.vector.riotx.core.utils.DimensionConverter
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    +import im.vector.riotx.features.settings.VectorPreferences
    +import me.gujun.android.span.span
    +import javax.inject.Inject
    +
    +class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
    +                                                         private val colorProvider: ColorProvider,
    +                                                         private val dimensionConverter: DimensionConverter,
    +                                                         private val vectorPreferences: VectorPreferences)
    +    : TypedEpoxyController() {
    +
    +    interface InteractionListener {
    +        fun onVerifyManually(device: CryptoDeviceInfo)
    +    }
    +
    +    var interactionListener: InteractionListener? = null
    +
    +    override fun buildModels(data: DeviceListViewState?) {
    +        data?.selectedDevice?.let {
    +            val isVerified = it.trustLevel?.isVerified() == true
    +            genericItem {
    +                id("title")
    +                style(GenericItem.STYLE.BIG_TEXT)
    +                titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
    +                title(
    +                        stringProvider.getString(
    +                                if (isVerified) R.string.verification_profile_verified else R.string.verification_profile_warning
    +                        )
    +                )
    +            }
    +            genericFooterItem {
    +                id("desc")
    +                centered(false)
    +                textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                apply {
    +                    if (isVerified) {
    +                        // TODO FORMAT
    +                        text(stringProvider.getString(R.string.verification_profile_device_verified_because,
    +                                data.userItem?.displayName ?: "",
    +                                data.userItem?.id ?: ""))
    +                    } else {
    +                        // TODO what if mine
    +                        text(stringProvider.getString(R.string.verification_profile_device_new_signing,
    +                                data.userItem?.displayName ?: "",
    +                                data.userItem?.id ?: ""))
    +                    }
    +                }
    +//                    text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
    +            }
    +
    +            genericItemWithValue {
    +                id(it.deviceId)
    +                titleIconResourceId(if (isVerified) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning)
    +                title(
    +                        span {
    +                            +(it.displayName() ?: "")
    +                            span {
    +                                text = " (${it.deviceId})"
    +                                textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                textSize = dimensionConverter.spToPx(14)
    +                            }
    +                        }
    +                )
    +            }
    +
    +            if (!isVerified) {
    +                genericFooterItem {
    +                    id("warn")
    +                    centered(false)
    +                    textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                    text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
    +                }
    +
    +                bottomSheetVerificationActionItem {
    +                    id("verify")
    +                    title(stringProvider.getString(R.string.verification_verify_device_manually))
    +                    titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_accent))
    +                    listener {
    +                        interactionListener?.onVerifyManually(it)
    +                    }
    +                }
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    index c734558c0e..816b5b3fee 100755
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt
    @@ -66,12 +66,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
             const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
             const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
             const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
    -        const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
    -        const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"
    +        const val SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
             const val SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"
             const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"
             const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
    -        const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"
    +        const val SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY =  "SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
     
             const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    index cf5273d5a4..e688b6b68e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt
    @@ -1,5 +1,6 @@
     /*
      * Copyright 2019 New Vector Ltd
    + * Copyright 2020 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.
    @@ -28,9 +29,10 @@ import androidx.preference.PreferenceCategory
     import androidx.preference.SwitchPreference
     import com.google.android.material.textfield.TextInputEditText
     import im.vector.matrix.android.api.MatrixCallback
    -import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
    +import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
     import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
    +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
     import im.vector.riotx.R
     import im.vector.riotx.core.dialogs.ExportKeysDialog
     import im.vector.riotx.core.intent.ExternalIntentData
    @@ -38,7 +40,12 @@ import im.vector.riotx.core.intent.analyseIntent
     import im.vector.riotx.core.intent.getFilenameFromUri
     import im.vector.riotx.core.platform.SimpleTextWatcher
     import im.vector.riotx.core.preference.VectorPreference
    -import im.vector.riotx.core.utils.*
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
    +import im.vector.riotx.core.utils.openFileSelection
    +import im.vector.riotx.core.utils.toast
     import im.vector.riotx.features.crypto.keys.KeysExporter
     import im.vector.riotx.features.crypto.keys.KeysImporter
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
    @@ -68,12 +75,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
         private val mPushersSettingsCategory by lazy {
             findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
         }
    -    private val cryptoInfoDeviceNamePreference by lazy {
    -        findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
    -    }
     
    -    private val cryptoInfoDeviceIdPreference by lazy {
    -        findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY)!!
    +    private val mCrossSigningStatePreference by lazy {
    +        findPreference(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!!
         }
     
         private val manageBackupPref by lazy {
    @@ -88,9 +92,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             findPreference(VectorPreferences.SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY)!!
         }
     
    -    private val cryptoInfoTextPreference by lazy {
    -        findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY)!!
    +    private val showDeviceListPref by lazy {
    +        findPreference(VectorPreferences.SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY)!!
         }
    +
         // encrypt to unverified devices
         private val sendToUnverifiedDevicesPref by lazy {
             findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
    @@ -100,6 +105,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
             super.onResume()
             // My device name may have been updated
             refreshMyDevice()
    +        refreshXSigningStatus()
    +        mCryptographyCategory.isVisible = vectorPreferences.developerMode()
         }
     
         override fun bindPref() {
    @@ -121,6 +128,35 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
                     true
                 }
             }
    +
    +        refreshXSigningStatus()
    +    }
    +
    +    private fun refreshXSigningStatus() {
    +        if (vectorPreferences.developerMode()) {
    +            val crossSigningKeys = session.getCrossSigningService().getMyCrossSigningKeys()
    +            val xSigningIsEnableInAccount = crossSigningKeys != null
    +            val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified()
    +            val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign()
    +
    +            if (xSigningKeyCanSign) {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
    +            } else if (xSigningKeysAreTrusted) {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_warning)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
    +            } else if (xSigningIsEnableInAccount) {
    +                mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
    +            } else {
    +                mCrossSigningStatePreference.setIcon(android.R.color.transparent)
    +                mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
    +            }
    +
    +            mCrossSigningStatePreference.isVisible = true
    +        } else {
    +            mCrossSigningStatePreference.isVisible = false
    +        }
         }
     
         override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    @@ -324,53 +360,55 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
          *
          * @param aMyDeviceInfo the device info
          */
    -    private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) {
    -        val userId = session.myUserId
    -        val deviceId = session.sessionParams.credentials.deviceId
    +    private fun refreshCryptographyPreference(devices: List) {
    +        showDeviceListPref.isEnabled = devices.size > 0
    +        showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size)
    +//        val userId = session.myUserId
    +//        val deviceId = session.sessionParams.credentials.deviceId
     
             // device name
    -        if (null != aMyDeviceInfo) {
    -            cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
    -
    -            cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    -                // TODO device can be rename only from the device list screen for the moment
    -                // displayDeviceRenameDialog(aMyDeviceInfo)
    -                true
    -            }
    -
    -            cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
    -                override fun onPreferenceLongClick(preference: Preference): Boolean {
    -                    activity?.let { copyToClipboard(it, aMyDeviceInfo.displayName!!) }
    -                    return true
    -                }
    -            }
    -        }
    -
    -        // crypto section: device ID
    -        if (!deviceId.isNullOrEmpty()) {
    -            cryptoInfoDeviceIdPreference.summary = deviceId
    -
    -            cryptoInfoDeviceIdPreference.setOnPreferenceClickListener {
    -                activity?.let { copyToClipboard(it, deviceId) }
    -                true
    -            }
    -        }
    -
    -        // crypto section: device key (fingerprint)
    -        if (!deviceId.isNullOrEmpty() && userId.isNotEmpty()) {
    -            val deviceInfo = session.getDeviceInfo(userId, deviceId)
    -
    -            if (null != deviceInfo && !deviceInfo.fingerprint().isNullOrEmpty()) {
    -                cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable()
    -
    -                cryptoInfoTextPreference.setOnPreferenceClickListener {
    -                    deviceInfo.fingerprint()?.let {
    -                        copyToClipboard(requireActivity(), it)
    -                    }
    -                    true
    -                }
    -            }
    -        }
    +//        if (null != aMyDeviceInfo) {
    +//            cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
    +//
    +//            cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
    +//                // TODO device can be rename only from the device list screen for the moment
    +//                // displayDeviceRenameDialog(aMyDeviceInfo)
    +//                true
    +//            }
    +//
    +//            cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
    +//                override fun onPreferenceLongClick(preference: Preference): Boolean {
    +//                    activity?.let { copyToClipboard(it, aMyDeviceInfo.displayName!!) }
    +//                    return true
    +//                }
    +//            }
    +//        }
    +//
    +//        // crypto section: device ID
    +//        if (!deviceId.isNullOrEmpty()) {
    +//            cryptoInfoDeviceIdPreference.summary = deviceId
    +//
    +//            cryptoInfoDeviceIdPreference.setOnPreferenceClickListener {
    +//                activity?.let { copyToClipboard(it, deviceId) }
    +//                true
    +//            }
    +//        }
    +//
    +//        // crypto section: device key (fingerprint)
    +//        if (!deviceId.isNullOrEmpty() && userId.isNotEmpty()) {
    +//            val deviceInfo = session.getDeviceInfo(userId, deviceId)
    +//
    +//            if (null != deviceInfo && !deviceInfo.fingerprint().isNullOrEmpty()) {
    +//                cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable()
    +//
    +//                cryptoInfoTextPreference.setOnPreferenceClickListener {
    +//                    deviceInfo.fingerprint()?.let {
    +//                        copyToClipboard(requireActivity(), it)
    +//                    }
    +//                    true
    +//                }
    +//            }
    +//        }
     
             sendToUnverifiedDevicesPref.isChecked = false
     
    @@ -389,18 +427,19 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
     
         private fun refreshMyDevice() {
             // TODO Move to a ViewModel...
    -        session.sessionParams.credentials.deviceId?.let {
    -            session.getDeviceInfo(it, object : MatrixCallback {
    -                override fun onFailure(failure: Throwable) {
    -                    // Ignore for this time?...
    +        session.getDevicesList(object : MatrixCallback {
    +            override fun onSuccess(data: DevicesListResponse) {
    +                if (isAdded) {
    +                    refreshCryptographyPreference(data.devices ?: emptyList())
                     }
    +            }
     
    -                override fun onSuccess(data: DeviceInfo) {
    -                    mMyDeviceInfo = data
    -                    refreshCryptographyPreference(data)
    +            override fun onFailure(failure: Throwable) {
    +                if (isAdded) {
    +                    refreshCryptographyPreference(emptyList())
                     }
    -            })
    -        }
    +            }
    +        })
         }
     
         // ==============================================================================================================
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt
    new file mode 100644
    index 0000000000..ccaa3d749d
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningEpoxyController.kt
    @@ -0,0 +1,173 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.crosssigning
    +
    +import com.airbnb.epoxy.TypedEpoxyController
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.genericItem
    +import im.vector.riotx.core.ui.list.genericItemWithValue
    +import im.vector.riotx.core.utils.DimensionConverter
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    +import me.gujun.android.span.span
    +import javax.inject.Inject
    +
    +class CrossSigningEpoxyController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider,
    +        private val dimensionConverter: DimensionConverter
    +) : TypedEpoxyController() {
    +
    +    interface InteractionListener {
    +        fun onInitializeCrossSigningKeys()
    +        fun onResetCrossSigningKeys()
    +    }
    +
    +    var interactionListener: InteractionListener? = null
    +
    +    override fun buildModels(data: CrossSigningSettingsViewState?) {
    +        if (data == null) return
    +        if (data.xSigningKeyCanSign) {
    +            genericItem {
    +                id("can")
    +                titleIconResourceId(R.drawable.ic_shield_trusted)
    +                title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
    +            }
    +            if (!data.isUploadingKeys) {
    +                bottomSheetVerificationActionItem {
    +                    id("resetkeys")
    +                    title("Reset keys")
    +                    titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    listener {
    +                        interactionListener?.onResetCrossSigningKeys()
    +                    }
    +                }
    +            }
    +        } else if (data.xSigningKeysAreTrusted) {
    +            genericItem {
    +                id("trusted")
    +                titleIconResourceId(R.drawable.ic_shield_warning)
    +                title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
    +            }
    +            if (!data.isUploadingKeys) {
    +                bottomSheetVerificationActionItem {
    +                    id("resetkeys")
    +                    title("Reset keys")
    +                    titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    listener {
    +                        interactionListener?.onResetCrossSigningKeys()
    +                    }
    +                }
    +            } else if (data.xSigningIsEnableInAccount) {
    +                genericItem {
    +                    id("enable")
    +                    titleIconResourceId(R.drawable.ic_shield_black)
    +                    title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
    +                }
    +                bottomSheetVerificationActionItem {
    +                    id("resetkeys")
    +                    title("Reset keys")
    +                    titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    listener {
    +                        interactionListener?.onResetCrossSigningKeys()
    +                    }
    +                }
    +            }
    +        } else {
    +            genericItem {
    +                id("not")
    +                title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
    +            }
    +            if (!data.isUploadingKeys) {
    +                bottomSheetVerificationActionItem {
    +                    id("initKeys")
    +                    title("Initialize keys")
    +                    titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
    +                    listener {
    +                        interactionListener?.onInitializeCrossSigningKeys()
    +                    }
    +                }
    +            }
    +        }
    +
    +        if (data.isUploadingKeys) {
    +            loadingItem {
    +                id("loading")
    +            }
    +        } else {
    +            val crossSigningKeys = data.crossSigningInfo
    +
    +            crossSigningKeys?.masterKey()?.let {
    +                genericItemWithValue {
    +                    id("msk")
    +                    titleIconResourceId(R.drawable.key_small)
    +                    title(
    +                            span {
    +                                +"Master Key:\n"
    +                                span {
    +                                    text = it.unpaddedBase64PublicKey ?: ""
    +                                    textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                    textSize = dimensionConverter.spToPx(12)
    +                                }
    +                            }
    +                    )
    +                }
    +            }
    +            crossSigningKeys?.userKey()?.let {
    +                genericItemWithValue {
    +                    id("usk")
    +                    titleIconResourceId(R.drawable.key_small)
    +                    title(
    +                            span {
    +                                +"User Key:\n"
    +                                span {
    +                                    text = it.unpaddedBase64PublicKey ?: ""
    +                                    textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                    textSize = dimensionConverter.spToPx(12)
    +                                }
    +                            }
    +                    )
    +                }
    +            }
    +            crossSigningKeys?.selfSigningKey()?.let {
    +                genericItemWithValue {
    +                    id("ssk")
    +                    titleIconResourceId(R.drawable.key_small)
    +                    title(
    +                            span {
    +                                +"Self Signed Key:\n"
    +                                span {
    +                                    text = it.unpaddedBase64PublicKey ?: ""
    +                                    textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
    +                                    textSize = dimensionConverter.spToPx(12)
    +                                }
    +                            }
    +                    )
    +                }
    +            }
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
    new file mode 100644
    index 0000000000..168855b201
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsFragment.kt
    @@ -0,0 +1,124 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.crosssigning
    +
    +import android.os.Bundle
    +import android.text.InputType
    +import android.view.View
    +import android.widget.EditText
    +import androidx.appcompat.app.AlertDialog
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.extensions.observeEvent
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import kotlinx.android.synthetic.main.fragment_generic_recycler.*
    +import javax.inject.Inject
    +
    +class CrossSigningSettingsFragment @Inject constructor(
    +        private val epoxyController: CrossSigningEpoxyController,
    +        val viewModelFactory: CrossSigningSettingsViewModel.Factory
    +) : VectorBaseFragment(), CrossSigningEpoxyController.InteractionListener {
    +
    +    override fun getLayoutResId() = R.layout.fragment_generic_recycler
    +
    +    private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        viewModel.requestLiveData.observeEvent(this) {
    +            when (it) {
    +                is Fail    -> {
    +                    AlertDialog.Builder(requireContext())
    +                            .setTitle(R.string.dialog_title_error)
    +                            .setMessage(it.error.message)
    +                            .setPositiveButton(R.string.ok, null)
    +                            .show()
    +                }
    +                is Success -> {
    +                    when (val action = it.invoke()) {
    +                        is CrossSigningAction.RequestPasswordAuth -> {
    +                            requestPassword(action.sessionId)
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun onResume() {
    +        super.onResume()
    +        (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.encryption_information_cross_signing_state)
    +    }
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        setupRecyclerView()
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        epoxyController.setData(state)
    +    }
    +
    +    private fun setupRecyclerView() {
    +        recyclerView.configureWith(epoxyController, hasFixedSize = false, disableItemAnimation = true)
    +        epoxyController.interactionListener = this
    +    }
    +
    +    override fun onDestroyView() {
    +        recyclerView.cleanup()
    +        epoxyController.interactionListener = null
    +        super.onDestroyView()
    +    }
    +
    +    fun requestPassword(sessionId: String) {
    +        // Ask for password
    +        val inflater = this.layoutInflater
    +        val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
    +
    +        val input = layout.findViewById(R.id.edit_text).also {
    +            it.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
    +        }
    +
    +        AlertDialog.Builder(requireContext())
    +                .setTitle("Confirm password")
    +                .setView(layout)
    +                .setPositiveButton(R.string.ok) { _, _ ->
    +                    val pass = input.text.toString()
    +
    +                    viewModel.handle(CrossSigningAction.InitializeCrossSigning(UserPasswordAuth(
    +                            session = sessionId,
    +                            password = pass
    +                    )))
    +                }
    +                .setNegativeButton(R.string.cancel, null)
    +                .show()
    +    }
    +
    +    override fun onInitializeCrossSigningKeys() {
    +        viewModel.handle(CrossSigningAction.InitializeCrossSigning())
    +    }
    +
    +    override fun onResetCrossSigningKeys() {
    +        viewModel.handle(CrossSigningAction.InitializeCrossSigning())
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt
    new file mode 100644
    index 0000000000..cba86da599
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/crosssigning/CrossSigningSettingsViewModel.kt
    @@ -0,0 +1,153 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.crosssigning
    +
    +import androidx.lifecycle.LiveData
    +import androidx.lifecycle.MutableLiveData
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.failure.Failure
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
    +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
    +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
    +import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
    +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
    +import im.vector.matrix.android.internal.di.MoshiProvider
    +import im.vector.matrix.rx.rx
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.utils.LiveEvent
    +
    +data class CrossSigningSettingsViewState(
    +        val crossSigningInfo: MXCrossSigningInfo? = null,
    +        val xSigningIsEnableInAccount: Boolean = false,
    +        val xSigningKeysAreTrusted: Boolean = false,
    +        val xSigningKeyCanSign: Boolean = true,
    +        val isUploadingKeys: Boolean = false
    +) : MvRxState
    +
    +sealed class CrossSigningAction : VectorViewModelAction {
    +    data class InitializeCrossSigning(val auth: UserPasswordAuth? = null) : CrossSigningAction()
    +    data class RequestPasswordAuth(val sessionId: String) : CrossSigningAction()
    +}
    +
    +class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
    +                                                                private val stringProvider: StringProvider,
    +                                                                private val session: Session)
    +    : VectorViewModel(initialState) {
    +
    +    // Can be used for several actions, for a one shot result
    +    private val _requestLiveData = MutableLiveData>>()
    +    val requestLiveData: LiveData>>
    +        get() = _requestLiveData
    +
    +    init {
    +        session.rx().liveCrossSigningInfo(session.myUserId)
    +                .execute {
    +                    val crossSigningKeys = it.invoke()?.getOrNull()
    +                    val xSigningIsEnableInAccount = crossSigningKeys != null
    +                    val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified()
    +                    val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign()
    +                    copy(
    +                            crossSigningInfo = crossSigningKeys,
    +                            xSigningIsEnableInAccount = xSigningIsEnableInAccount,
    +                            xSigningKeysAreTrusted = xSigningKeysAreTrusted,
    +                            xSigningKeyCanSign = xSigningKeyCanSign
    +                    )
    +                }
    +    }
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
    +    }
    +
    +    override fun handle(action: CrossSigningAction) {
    +        when (action) {
    +            is CrossSigningAction.InitializeCrossSigning -> {
    +                initializeCrossSigning(action.auth?.also { it.user = session.myUserId })
    +            }
    +        }
    +    }
    +
    +    private fun initializeCrossSigning(auth: UserPasswordAuth?) {
    +        setState {
    +            copy(isUploadingKeys = true)
    +        }
    +        session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback {
    +            override fun onSuccess(data: Unit) {
    +                setState {
    +                    copy(isUploadingKeys = false)
    +                }
    +            }
    +
    +            override fun onFailure(failure: Throwable) {
    +                if (failure is Failure.OtherServerError
    +                        && failure.httpCode == 401
    +                ) {
    +                    try {
    +                        MoshiProvider.providesMoshi()
    +                                .adapter(RegistrationFlowResponse::class.java)
    +                                .fromJson(failure.errorBody)
    +                    } catch (e: Exception) {
    +                        null
    +                    }?.let { flowResponse ->
    +                        // Retry with authentication
    +                        if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
    +                            _requestLiveData.postValue(LiveEvent(Success(CrossSigningAction.RequestPasswordAuth(flowResponse.session ?: ""))))
    +                            return
    +                        } else {
    +                            _requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile"))))
    +                            // can't do this from here
    +                            return
    +                        }
    +                    }
    +                }
    +                when (failure) {
    +                    is Failure.ServerError -> {
    +                        _requestLiveData.postValue(LiveEvent(Fail(Throwable(failure.error.message))))
    +                    }
    +                    else                   -> {
    +                        _requestLiveData.postValue(LiveEvent(Fail(failure)))
    +                    }
    +                }
    +                setState {
    +                    copy(isUploadingKeys = false)
    +                }
    +            }
    +        })
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: CrossSigningSettingsViewState): CrossSigningSettingsViewModel? {
    +            val fragment: CrossSigningSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.viewModelFactory.create(state)
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt
    index b6c84ade9a..267c0eb676 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt
    @@ -17,9 +17,11 @@
     package im.vector.riotx.features.settings.devices
     
     import android.graphics.Typeface
    -import android.view.View
     import android.view.ViewGroup
    +import android.widget.ImageView
     import android.widget.TextView
    +import androidx.core.content.ContextCompat
    +import androidx.core.view.isInvisible
     import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    @@ -29,7 +31,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
     import java.text.DateFormat
     import java.text.SimpleDateFormat
    -import java.util.*
    +import java.util.Date
    +import java.util.Locale
     
     /**
      * A list item for Device.
    @@ -43,67 +46,81 @@ abstract class DeviceItem : VectorEpoxyModel() {
         @EpoxyAttribute
         var currentDevice = false
     
    -    @EpoxyAttribute
    -    var buttonsVisible = false
    -
         @EpoxyAttribute
         var itemClickAction: (() -> Unit)? = null
     
         @EpoxyAttribute
    -    var renameClickAction: (() -> Unit)? = null
    +    var detailedMode = false
     
         @EpoxyAttribute
    -    var deleteClickAction: (() -> Unit)? = null
    +    var trusted : Boolean? = false
     
         override fun bind(holder: Holder) {
             holder.root.setOnClickListener { itemClickAction?.invoke() }
     
    -        holder.displayNameText.text = deviceInfo.displayName ?: ""
    -        holder.deviceIdText.text = deviceInfo.deviceId ?: ""
    +        if (trusted != null) {
    +            holder.trustIcon.setImageDrawable(
    +                    ContextCompat.getDrawable(
    +                            holder.view.context,
    +                            if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
    +                    )
    +            )
    +            holder.trustIcon.isInvisible = false
    +        } else {
    +            holder.trustIcon.isInvisible = true
    +        }
     
    -        val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
    -
    -        val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
    -            val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
    -            val date = Date(ts)
    -
    -            val time = dateFormatTime.format(date)
    -            val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
    -
    -            dateFormat.format(date) + ", " + time
    -        } ?: "-"
    -
    -        holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
    -
    -        listOf(
    +        val detailedModeLabels = listOf(
                     holder.displayNameLabelText,
                     holder.displayNameText,
                     holder.deviceIdLabelText,
                     holder.deviceIdText,
                     holder.deviceLastSeenLabelText,
                     holder.deviceLastSeenText
    -        ).map {
    -            it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
    +        )
    +        if (detailedMode) {
    +            holder.summaryLabelText.isVisible = false
    +
    +            holder.displayNameText.text = deviceInfo.displayName ?: ""
    +            holder.deviceIdText.text = deviceInfo.deviceId ?: ""
    +
    +            val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
    +
    +            val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
    +                val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
    +                val date = Date(ts)
    +
    +                val time = dateFormatTime.format(date)
    +                val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
    +
    +                dateFormat.format(date) + ", " + time
    +            } ?: "-"
    +
    +            holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
    +
    +            detailedModeLabels.map {
    +                it.isVisible = true
    +                it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
    +            }
    +        } else {
    +            holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: ""
    +            holder.summaryLabelText.isVisible = true
    +            detailedModeLabels.map {
    +                it.isVisible = false
    +            }
             }
    -
    -        holder.buttonDelete.isVisible = !currentDevice
    -
    -        holder.buttons.isVisible = buttonsVisible
    -
    -        holder.buttonRename.setOnClickListener { renameClickAction?.invoke() }
    -        holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() }
         }
     
         class Holder : VectorEpoxyHolder() {
             val root by bind(R.id.itemDeviceRoot)
    +        val summaryLabelText by bind(R.id.itemDeviceSimpleSummary)
             val displayNameLabelText by bind(R.id.itemDeviceDisplayNameLabel)
             val displayNameText by bind(R.id.itemDeviceDisplayName)
             val deviceIdLabelText by bind(R.id.itemDeviceIdLabel)
             val deviceIdText by bind(R.id.itemDeviceId)
             val deviceLastSeenLabelText by bind(R.id.itemDeviceLastSeenLabel)
             val deviceLastSeenText by bind(R.id.itemDeviceLastSeen)
    -        val buttons by bind(R.id.itemDeviceButtons)
    -        val buttonDelete by bind(R.id.itemDeviceDelete)
    -        val buttonRename by bind(R.id.itemDeviceRename)
    +
    +        val trustIcon by bind(R.id.itemDeviceTrustLevelIcon)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt
    new file mode 100644
    index 0000000000..9863134cb7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheet.kt
    @@ -0,0 +1,94 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.devices
    +
    +import android.os.Bundle
    +import android.os.Parcelable
    +import androidx.core.view.isVisible
    +import androidx.recyclerview.widget.RecyclerView
    +import butterknife.BindView
    +import com.airbnb.mvrx.MvRx
    +import com.airbnb.mvrx.fragmentViewModel
    +import com.airbnb.mvrx.parentFragmentViewModel
    +import com.airbnb.mvrx.withState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import kotlinx.android.parcel.Parcelize
    +import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
    +import javax.inject.Inject
    +
    +@Parcelize
    +data class DeviceVerificationInfoArgs(
    +        val userId: String,
    +        val deviceId: String
    +) : Parcelable
    +
    +class DeviceVerificationInfoBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceVerificationInfoEpoxyController.Callback {
    +
    +    private val viewModel: DeviceVerificationInfoBottomSheetViewModel by fragmentViewModel(DeviceVerificationInfoBottomSheetViewModel::class)
    +
    +    private val sharedViewModel: DevicesViewModel by parentFragmentViewModel(DevicesViewModel::class)
    +
    +    @Inject lateinit var deviceVerificationInfoViewModelFactory: DeviceVerificationInfoBottomSheetViewModel.Factory
    +
    +    @BindView(R.id.bottomSheetRecyclerView)
    +    lateinit var recyclerView: RecyclerView
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    @Inject lateinit var epoxyController: DeviceVerificationInfoEpoxyController
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
    +
    +    override fun onActivityCreated(savedInstanceState: Bundle?) {
    +        super.onActivityCreated(savedInstanceState)
    +        recyclerView.configureWith(
    +                epoxyController,
    +                showDivider = true,
    +                hasFixedSize = false)
    +        epoxyController.callback = this
    +        bottomSheetTitle.isVisible = false
    +    }
    +
    +    override fun onDestroyView() {
    +        recyclerView.cleanup()
    +        super.onDestroyView()
    +    }
    +
    +    override fun invalidate() = withState(viewModel) {
    +        epoxyController.setData(it)
    +        super.invalidate()
    +    }
    +
    +    companion object {
    +        fun newInstance(userId: String, deviceId: String): DeviceVerificationInfoBottomSheet {
    +            val args = Bundle()
    +            val parcelableArgs = DeviceVerificationInfoArgs(userId, deviceId)
    +            args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
    +            return DeviceVerificationInfoBottomSheet().apply { arguments = args }
    +        }
    +    }
    +
    +    override fun onAction(action: DevicesAction) {
    +        dismiss()
    +        sharedViewModel.handle(action)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt
    new file mode 100644
    index 0000000000..b4eee1fdf7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt
    @@ -0,0 +1,68 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.devices
    +
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.Uninitialized
    +import com.airbnb.mvrx.ViewModelContext
    +import com.squareup.inject.assisted.Assisted
    +import com.squareup.inject.assisted.AssistedInject
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.riotx.core.di.HasScreenInjector
    +import im.vector.riotx.core.platform.EmptyAction
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +
    +data class DeviceVerificationInfoBottomSheetViewState(
    +        val cryptoDeviceInfo: Async = Uninitialized
    +) : MvRxState
    +
    +class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
    +                                                                             val session: Session
    +) : VectorViewModel(initialState) {
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: DeviceVerificationInfoBottomSheetViewState): DeviceVerificationInfoBottomSheetViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        @JvmStatic
    +        override fun create(viewModelContext: ViewModelContext, state: DeviceVerificationInfoBottomSheetViewState)
    +                : DeviceVerificationInfoBottomSheetViewModel? {
    +            val fragment: DeviceVerificationInfoBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.deviceVerificationInfoViewModelFactory.create(state)
    +        }
    +
    +        override fun initialState(viewModelContext: ViewModelContext): DeviceVerificationInfoBottomSheetViewState? {
    +            val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    +            val args = viewModelContext.args()
    +            session.getDeviceInfo(args.userId, args.deviceId)?.let {
    +                return DeviceVerificationInfoBottomSheetViewState(cryptoDeviceInfo = Success(it))
    +            }
    +            return super.initialState(viewModelContext)
    +        }
    +    }
    +
    +    override fun handle(action: EmptyAction) {
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt
    new file mode 100644
    index 0000000000..d2e7fff215
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoEpoxyController.kt
    @@ -0,0 +1,120 @@
    +/*
    + * Copyright 2020 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.riotx.features.settings.devices
    +
    +import com.airbnb.epoxy.TypedEpoxyController
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.dividerItem
    +import im.vector.riotx.core.epoxy.loadingItem
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.ui.list.GenericItem
    +import im.vector.riotx.core.ui.list.genericItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    +import javax.inject.Inject
    +
    +class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
    +                                                                private val colorProvider: ColorProvider,
    +                                                                private val session: Session)
    +    : TypedEpoxyController() {
    +
    +    var callback: Callback? = null
    +
    +    override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
    +        val device = data?.cryptoDeviceInfo?.invoke()
    +        if (device == null) {
    +            loadingItem {
    +                id("loading")
    +            }
    +        } else {
    +            if (device.isVerified) {
    +                genericItem {
    +                    id("trust${device.deviceId}")
    +                    style(GenericItem.STYLE.BIG_TEXT)
    +                    titleIconResourceId(R.drawable.ic_shield_trusted)
    +                    title(stringProvider.getString(R.string.encryption_information_verified))
    +                    description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
    +                }
    +            } else {
    +                genericItem {
    +                    id("trust${device.deviceId}")
    +                    titleIconResourceId(R.drawable.ic_shield_warning)
    +                    style(GenericItem.STYLE.BIG_TEXT)
    +                    title(stringProvider.getString(R.string.encryption_information_not_verified))
    +                    description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
    +                }
    +            }
    +
    +            genericItem {
    +                id("info${device.deviceId}")
    +                title(device.displayName() ?: "")
    +                description("(${device.deviceId})")
    +            }
    +
    +            if (!device.isVerified) {
    +                dividerItem {
    +                    id("d1")
    +                }
    +                bottomSheetVerificationActionItem {
    +                    id("verify")
    +                    title(stringProvider.getString(R.string.verification_verify_device))
    +                    titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_accent))
    +                    listener {
    +                        callback?.onAction(DevicesAction.VerifyMyDevice(device.deviceId))
    +                    }
    +                }
    +            }
    +
    +            if (device.deviceId != session.sessionParams.credentials.deviceId) {
    +                // Add the delete option
    +                dividerItem {
    +                    id("d2")
    +                }
    +                bottomSheetVerificationActionItem {
    +                    id("delete")
    +                    title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
    +                    titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    iconRes(R.drawable.ic_arrow_right)
    +                    iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
    +                    listener {
    +                        callback?.onAction(DevicesAction.Delete(device.deviceId))
    +                    }
    +                }
    +            }
    +
    +            dividerItem {
    +                id("d3")
    +            }
    +            bottomSheetVerificationActionItem {
    +                id("rename")
    +                title(stringProvider.getString(R.string.rename))
    +                titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                listener {
    +                   callback?.onAction(DevicesAction.PromptRename(device.deviceId))
    +                }
    +            }
    +        }
    +    }
    +
    +    interface Callback {
    +        fun onAction(action: DevicesAction)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt
    index 550703039f..29fbb9ca46 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt
    @@ -21,8 +21,9 @@ import im.vector.riotx.core.platform.VectorViewModelAction
     
     sealed class DevicesAction : VectorViewModelAction {
         object Retry : DevicesAction()
    -    data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
    +    data class Delete(val deviceId: String) : DevicesAction()
         data class Password(val password: String) : DevicesAction()
         data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
    -    data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
    +    data class PromptRename(val deviceId: String, val deviceInfo: DeviceInfo? = null) : DevicesAction()
    +    data class VerifyMyDevice(val deviceId: String, val userId: String? = null, val transactionId: String? = null) : DevicesAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt
    index 18c0965f86..769a540a1a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt
    @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.Uninitialized
     import im.vector.matrix.android.api.extensions.sortByLastSeen
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.errorWithRetryItem
    @@ -29,10 +30,12 @@ import im.vector.riotx.core.epoxy.loadingItem
     import im.vector.riotx.core.error.ErrorFormatter
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.ui.list.genericItemHeader
    +import im.vector.riotx.features.settings.VectorPreferences
     import javax.inject.Inject
     
     class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
    -                                            private val stringProvider: StringProvider) : EpoxyController() {
    +                                            private val stringProvider: StringProvider,
    +                                            private val vectorPreferences: VectorPreferences) : EpoxyController() {
     
         var callback: Callback? = null
         private var viewState: DevicesViewState? = null
    @@ -65,11 +68,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
                         listener { callback?.retry() }
                     }
                 is Success       ->
    -                buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId)
    +                buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId)
             }
         }
     
    -    private fun buildDevicesList(devices: List, myDeviceId: String, currentExpandedDeviceId: String?) {
    +    private fun buildDevicesList(devices: List, cryptoDevices: List?, myDeviceId: String) {
             // Current device
             genericItemHeader {
                 id("current")
    @@ -83,12 +86,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
                     .forEachIndexed { idx, deviceInfo ->
                         deviceItem {
                             id("myDevice$idx")
    +                        detailedMode(vectorPreferences.developerMode())
                             deviceInfo(deviceInfo)
                             currentDevice(true)
    -                        buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
                             itemClickAction { callback?.onDeviceClicked(deviceInfo) }
    -                        renameClickAction { callback?.onRenameDevice(deviceInfo) }
    -                        deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
    +                        trusted(true)
                         }
                     }
     
    @@ -109,12 +111,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
                             val isCurrentDevice = deviceInfo.deviceId == myDeviceId
                             deviceItem {
                                 id("device$idx")
    +                            detailedMode(vectorPreferences.developerMode())
                                 deviceInfo(deviceInfo)
                                 currentDevice(isCurrentDevice)
    -                            buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
                                 itemClickAction { callback?.onDeviceClicked(deviceInfo) }
    -                            renameClickAction { callback?.onRenameDevice(deviceInfo) }
    -                            deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
    +                            trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified ?: false)
                             }
                         }
             }
    @@ -123,7 +124,5 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
         interface Callback {
             fun retry()
             fun onDeviceClicked(deviceInfo: DeviceInfo)
    -        fun onRenameDevice(deviceInfo: DeviceInfo)
    -        fun onDeleteDevice(deviceInfo: DeviceInfo)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    index 0324036347..e24f561a70 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt
    @@ -32,25 +32,30 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.failure.Failure
     import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationService
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
     import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
     import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
     import im.vector.riotx.core.extensions.postLiveEvent
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.utils.LiveEvent
    -import timber.log.Timber
     
     data class DevicesViewState(
             val myDeviceId: String = "",
             val devices: Async> = Uninitialized,
    -        val currentExpandedDeviceId: String? = null,
    +        val cryptoDevices: Async> = Uninitialized,
             // TODO Replace by isLoading boolean
             val request: Async = Uninitialized
     ) : MvRxState
     
     class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
                                                        private val session: Session)
    -    : VectorViewModel(initialState) {
    +    : VectorViewModel(initialState), VerificationService.VerificationListener {
     
         @AssistedInject.Factory
         interface Factory {
    @@ -74,8 +79,26 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
         val requestPasswordLiveData: LiveData>
             get() = _requestPasswordLiveData
     
    +    // Used to communicate back from model to fragment
    +    private val _requestLiveData = MutableLiveData>>()
    +    val fragmentActionLiveData: LiveData>>
    +        get() = _requestLiveData
    +
         init {
             refreshDevicesList()
    +        session.getVerificationService().addListener(this)
    +    }
    +
    +    override fun onCleared() {
    +        session.getVerificationService().removeListener(this)
    +        super.onCleared()
    +    }
    +
    +    override fun transactionCreated(tx: VerificationTransaction) {}
    +    override fun transactionUpdated(tx: VerificationTransaction) {
    +      if (tx.state == VerificationTxState.Verified) {
    +          refreshDevicesList()
    +      }
         }
     
         /**
    @@ -109,6 +132,25 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
                         }
                     }
                 })
    +
    +            // Put cached state
    +            setState {
    +                copy(
    +                        myDeviceId = session.sessionParams.credentials.deviceId ?: "",
    +                        cryptoDevices = Success(session.getUserDevices(session.myUserId))
    +                )
    +            }
    +
    +            // then force download
    +            session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
    +                override fun onSuccess(data: MXUsersDevicesMap) {
    +                    setState {
    +                        copy(
    +                                cryptoDevices = Success(session.getUserDevices(session.myUserId))
    +                        )
    +                    }
    +                }
    +            })
             } else {
                 // Should not happen
             }
    @@ -116,21 +158,34 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
     
         override fun handle(action: DevicesAction) {
             return when (action) {
    -            is DevicesAction.Retry        -> refreshDevicesList()
    -            is DevicesAction.Delete       -> handleDelete(action)
    -            is DevicesAction.Password     -> handlePassword(action)
    -            is DevicesAction.Rename       -> handleRename(action)
    -            is DevicesAction.ToggleDevice -> handleToggleDevice(action)
    +            is DevicesAction.Retry          -> refreshDevicesList()
    +            is DevicesAction.Delete         -> handleDelete(action)
    +            is DevicesAction.Password       -> handlePassword(action)
    +            is DevicesAction.Rename         -> handleRename(action)
    +            is DevicesAction.PromptRename   -> handlePromptRename(action)
    +            is DevicesAction.VerifyMyDevice -> handleVerify(action)
             }
         }
     
    -    private fun handleToggleDevice(action: DevicesAction.ToggleDevice) {
    -        withState {
    -            setState {
    -                copy(
    -                        currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId
    -                )
    -            }
    +    private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
    +        // TODO Implement request in to DEVICE!!!
    +        val txID = session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId)
    +        if (txID != null) {
    +            _requestLiveData.postValue(LiveEvent(Success(
    +                    action.copy(
    +                            userId = session.myUserId,
    +                            transactionId = txID
    +                    )
    +            )))
    +        }
    +    }
    +
    +    private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
    +        val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
    +        if (info == null) {
    +            _requestLiveData.postValue(LiveEvent(Uninitialized))
    +        } else {
    +            _requestLiveData.postValue(LiveEvent(Success(action.copy(deviceInfo = info))))
             }
         }
     
    @@ -162,11 +217,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
          * Try to delete a device.
          */
         private fun handleDelete(action: DevicesAction.Delete) {
    -        val deviceId = action.deviceInfo.deviceId
    -        if (deviceId == null) {
    -            Timber.e("## handleDelete(): sanity check failure")
    -            return
    -        }
    +        val deviceId = action.deviceId
     
             setState {
                 copy(
    diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt
    index f24fb2f542..e9226773a1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt
    @@ -25,6 +25,7 @@ import androidx.appcompat.app.AlertDialog
     import androidx.core.view.isVisible
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
    @@ -36,6 +37,7 @@ import im.vector.riotx.core.extensions.observeEvent
     import im.vector.riotx.core.platform.VectorBaseActivity
     import im.vector.riotx.core.platform.VectorBaseFragment
     import im.vector.riotx.core.utils.toast
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     import kotlinx.android.synthetic.main.fragment_generic_recycler.*
     import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
     import javax.inject.Inject
    @@ -71,6 +73,29 @@ class VectorSettingsDevicesFragment @Inject constructor(
             viewModel.requestPasswordLiveData.observeEvent(this) {
                 maybeShowDeleteDeviceWithPasswordDialog()
             }
    +
    +        viewModel.fragmentActionLiveData.observeEvent(this) { async ->
    +            when (async) {
    +                is Success -> {
    +                    when (val action = async.invoke()) {
    +                        is DevicesAction.PromptRename   -> {
    +                            action.deviceInfo?.let { deviceInfo ->
    +                                displayDeviceRenameDialog(deviceInfo)
    +                            }
    +                        }
    +                        is DevicesAction.VerifyMyDevice -> {
    +                            if (context is VectorBaseActivity) {
    +                                VerificationBottomSheet.withArgs(
    +                                        roomId = null,
    +                                        otherUserId = action.userId!!,
    +                                        transactionId = action.transactionId!!
    +                                ).show(childFragmentManager, "REQPOP")
    +                            }
    +                        }
    +                    }
    +                }
    +            }
    +        }
         }
     
         override fun showFailure(throwable: Throwable) {
    @@ -89,20 +114,23 @@ class VectorSettingsDevicesFragment @Inject constructor(
         override fun onResume() {
             super.onResume()
     
    -        (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list)
    +        (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
         }
     
         override fun onDeviceClicked(deviceInfo: DeviceInfo) {
    -        viewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
    +        DeviceVerificationInfoBottomSheet.newInstance(deviceInfo.user_id ?: "", deviceInfo.deviceId ?: "").show(
    +                childFragmentManager,
    +                "VERIF_INFO"
    +        )
         }
     
    -    override fun onDeleteDevice(deviceInfo: DeviceInfo) {
    -        viewModel.handle(DevicesAction.Delete(deviceInfo))
    -    }
    -
    -    override fun onRenameDevice(deviceInfo: DeviceInfo) {
    -        displayDeviceRenameDialog(deviceInfo)
    -    }
    +//    override fun onDeleteDevice(deviceInfo: DeviceInfo) {
    +//        devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
    +//    }
    +//
    +//    override fun onRenameDevice(deviceInfo: DeviceInfo) {
    +//        displayDeviceRenameDialog(deviceInfo)
    +//    }
     
         override fun retry() {
             viewModel.handle(DevicesAction.Retry)
    diff --git a/vector/src/main/res/layout/bottom_sheet_with_fragments.xml b/vector/src/main/res/layout/bottom_sheet_with_fragments.xml
    new file mode 100644
    index 0000000000..e013b2a7b1
    --- /dev/null
    +++ b/vector/src/main/res/layout/bottom_sheet_with_fragments.xml
    @@ -0,0 +1,7 @@
    +
    +
    +
    +
    diff --git a/vector/src/main/res/layout/fragment_generic_recycler.xml b/vector/src/main/res/layout/fragment_generic_recycler.xml
    index b06e82f9ce..1c2dcc1c3a 100644
    --- a/vector/src/main/res/layout/fragment_generic_recycler.xml
    +++ b/vector/src/main/res/layout/fragment_generic_recycler.xml
    @@ -3,6 +3,7 @@
         xmlns:app="http://schemas.android.com/apk/res-auto"
         xmlns:tools="http://schemas.android.com/tools"
         android:layout_width="match_parent"
    +    android:background="?vctr_list_header_background_color"
         android:layout_height="match_parent">
     
         
    +
    +
    +    
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml
    index bebaf156d9..9917cc0e94 100644
    --- a/vector/src/main/res/layout/item_device.xml
    +++ b/vector/src/main/res/layout/item_device.xml
    @@ -6,93 +6,90 @@
         android:layout_height="wrap_content"
         android:background="?riotx_background"
         android:foreground="?attr/selectableItemBackground"
    -    android:orientation="vertical"
    -    android:paddingStart="16dp"
    -    android:paddingTop="8dp"
    -    android:paddingEnd="16dp"
    -    android:paddingBottom="8dp">
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    -
    -    
    +    android:orientation="horizontal"
    +    android:padding="8dp">
     
         
    +        android:layout_weight="1"
    +        android:orientation="vertical">
     
    -        
    +        
    +            android:gravity="center_vertical"
    +            android:minHeight="40dp"
    +            android:textColor="?riotx_text_primary"
    +            android:textSize="16sp"
    +            tools:text="Riot X" />
     
    -        
    +        
    +            android:layout_marginTop="6dp"
    +            android:text="@string/devices_details_name_title"
    +            android:textColor="?riotx_text_secondary"
    +            android:textSize="12sp" />
    +
    +        
    +
    +        
    +
    +        
    +
    +        
    +
    +        
     
         
     
    -
    \ No newline at end of file
    +    
    +
    +
    +
    diff --git a/vector/src/main/res/layout/item_generic_list.xml b/vector/src/main/res/layout/item_generic_list.xml
    index 8fdea1c17d..075cbe6da5 100644
    --- a/vector/src/main/res/layout/item_generic_list.xml
    +++ b/vector/src/main/res/layout/item_generic_list.xml
    @@ -8,24 +8,36 @@
         android:background="?android:attr/colorBackground"
         android:minHeight="50dp">
     
    +    
    +
         
    +
    +
    +    
    +
    +    
    +
    +
    +    
    +
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_profile_action.xml b/vector/src/main/res/layout/item_profile_action.xml
    index 2e32b4b444..a2499f2e83 100644
    --- a/vector/src/main/res/layout/item_profile_action.xml
    +++ b/vector/src/main/res/layout/item_profile_action.xml
    @@ -20,7 +20,6 @@
             android:layout_height="wrap_content"
             android:layout_centerVertical="true"
             android:scaleType="center"
    -        android:tint="?riotx_text_secondary"
             android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toStartOf="parent"
    diff --git a/vector/src/main/res/layout/item_verification_action.xml b/vector/src/main/res/layout/item_verification_action.xml
    index a92379a2a6..aad83d1b80 100644
    --- a/vector/src/main/res/layout/item_verification_action.xml
    +++ b/vector/src/main/res/layout/item_verification_action.xml
    @@ -7,7 +7,7 @@
         android:clickable="true"
         android:focusable="true"
         android:foreground="?attr/selectableItemBackground"
    -    android:minHeight="72dp"
    +    android:minHeight="64dp"
         android:paddingLeft="@dimen/layout_horizontal_margin"
         android:paddingTop="8dp"
         android:paddingRight="@dimen/layout_horizontal_margin"
    diff --git a/vector/src/main/res/layout/item_verification_big_image.xml b/vector/src/main/res/layout/item_verification_big_image.xml
    index 9f33b6c03c..e4a0db917f 100644
    --- a/vector/src/main/res/layout/item_verification_big_image.xml
    +++ b/vector/src/main/res/layout/item_verification_big_image.xml
    @@ -1,7 +1,8 @@
     
     
    +    tools:src="@drawable/ic_shield_trusted" />
    diff --git a/vector/src/main/res/layout/item_verification_qr_code.xml b/vector/src/main/res/layout/item_verification_qr_code.xml
    new file mode 100644
    index 0000000000..6a16315185
    --- /dev/null
    +++ b/vector/src/main/res/layout/item_verification_qr_code.xml
    @@ -0,0 +1,16 @@
    +
    +
    +
    +    
    +
    +
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 472f13018f..628493397f 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -32,6 +32,10 @@
         Verification Sent
         Verification Request
     
    +
    +    Verify this device
    +    Manually verify
    +
         
         You
     
    @@ -40,7 +44,7 @@
         Can\'t scan
         If you\'re not in person, compare emoji instead
     
    -    Continue
    +    Verify by comparing emojis
     
         Verify by Emoji
         If you can’t scan the code above, verify by comparing a short, unique selection of emoji.
    @@ -52,7 +56,7 @@
         Waiting for %s…
         For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.
         Messages in this room are not end-to-end encrypted.
    -    Messages in this room are end-to-end encrypted.
    +    Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.
         Security
         Learn more
         More
    @@ -96,7 +100,6 @@
         Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly.
         Enable encryption
     
    -
         For extra security, verify %s by checking a one-time code.
         For maximum security, do this in person.
     
    @@ -104,4 +107,44 @@
         Compare the code with the one displayed on the other user\'s screen.
         Messages with this user are end-to-end encrypted and can\'t be read by third parties.
     
    +
    +    Cross-Signing
    +    Cross-Signing is enabled\nPrivate Keys on device.
    +    Cross-Signing is enabled\nKeys are trusted\n.Private keys are not known
    +    Cross-Signing is enabled\nKeys are not trusted
    +    Cross-Signing is not enabled
    +
    +
    +    Active Sessions
    +    Show All Sessions
    +    Manage Sessions
    +    Sign out this device
    +
    +    This session is trusted for secure messaging because you verified it:
    +    Verify this session to mark it as trusted & grant it access to encrypted messages. If you didn’t sign in to this device your account may be compromised:
    +
    +    
    +        %d active session
    +        %d active sessions
    +    
    +
    +
    +    Verify
    +    Verified
    +    Warning
    +
    +    Failed to get devices
    +    Sessions
    +    Trusted
    +    Not Trusted
    +
    +    This device is trusted for secure messaging because %1$s (%2$s) verified it:
    +    %1$s (%2$s) signed in using a new device:
    +    Until this user trusts this device, messages sent to and from it are labelled with warnings. Alternatively, you can manually verify it.
    +
    +
    +    Initialize CrossSigning
    +    Reset Keys
    +
    +    QR code
     
    diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml
    index 234ecbe647..19bc340500 100644
    --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml
    +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml
    @@ -1,40 +1,52 @@
     
     
    +    xmlns:app="http://schemas.android.com/apk/res-auto"
    +    xmlns:tools="http://schemas.android.com/tools">
     
         
         
    -
             
    +            android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
    +            tools:icon="@drawable/ic_shield_trusted"
    +            android:persistent="false"
    +            android:title="@string/encryption_information_cross_signing_state"
    +            tools:summary="@string/encryption_information_dg_xsigning_complete"
    +            app:fragment="im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment"
    +            />
     
    -        
     
    -        
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
     
             
    +            android:enabled="false" />
     
         
     
         
         
    +        android:title="@string/settings_active_sessions_list">