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/CHANGES.md b/CHANGES.md index 7c235d8704..d291712305 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,43 @@ -Changes in RiotX 0.14.0 (2020-XX-XX) +Changes in RiotX 0.15.0 (2020-XX-XX) =================================================== Features ✨: + - + +Improvements 🙌: + - + +Other changes: + - + +Bugfix 🐛: + - + +Translations 🗣: + - + +Build 🧱: + - + +Changes in RiotX 0.14.2 (2020-02-02) +=================================================== + +Fix RiotX not starting issue + +Changes in RiotX 0.14.1 (2020-02-02) +=================================================== + +Bugfix 🐛: + - Cross-signing: fix UX issue when closing the bottom sheet verification (#813) + - Room and room member profile: fix issues on dark and black themes + +Changes in RiotX 0.14.0 (2020-02-01) +=================================================== + +Features ✨: + - First implementation of Cross-signing - Enable encryption in unencrypted rooms, from the room settings (#212) - - Enable e2e by default when creating DM, and give the possibility to enable encryption when creating room (#837) + - Negotiate E2E by default for DMs (#907) Improvements 🙌: - Sharing things to RiotX: sort list by recent room first (#771) @@ -13,12 +47,6 @@ Improvements 🙌: Other changes: - Add support for /rainbow and /rainbowme commands (#879) -Bugfix 🐛: - - - -Translations 🗣: - - - Build 🧱: - Ensure builds are reproducible (#842) - F-Droid: fix the "-dev" issue in version name (#815) @@ -120,6 +148,7 @@ Changes in RiotX 0.9.0 (2019-12-05) Features ✨: - Account creation. It's now possible to create account on any homeserver with RiotX (#34) - Iteration of the login flow (#613) + - [SDK] MSC2241 / verification in DMs (#707) Improvements 🙌: - Send mention Pills from composer @@ -325,7 +354,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a ======================================================= -Changes in RiotX 0.0.0 (2020-XX-XX) +Changes in RiotX 0.X.0 (2020-XX-XX) =================================================== Features ✨: diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 1d8e81e44f..8907ee8b50 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -41,6 +41,9 @@ dependencies { // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.0" + // Logging + implementation 'com.jakewharton.timber:timber:4.7.1' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index be74b9f21b..d6c05fb0f7 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -16,6 +16,8 @@ package im.vector.matrix.rx +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel +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.room.Room import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams @@ -28,23 +30,103 @@ import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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 +import io.reactivex.functions.BiFunction +import timber.log.Timber -class RxRoom(private val room: Room) { +class RxRoom(private val room: Room, private val session: Session) { fun liveRoomSummary(): Observable> { - return room.getRoomSummaryLive().asObservable() + val summaryObservable = room.getRoomSummaryLive() + .asObservable() .startWithCallable { room.roomSummary().toOptional() } + .doOnNext { Timber.v("RX: summary emitted for: ${it.getOrNull()?.roomId}") } + + val memberIdsChangeObservable = summaryObservable + .map { + it.getOrNull()?.let { roomSummary -> + if (roomSummary.isEncrypted) { + // Return the list of other users + roomSummary.otherMemberIds + listOf(session.myUserId) + } else { + // Return an empty list, the room is not encrypted + emptyList() + } + }.orEmpty() + }.distinctUntilChanged() + .doOnNext { Timber.v("RX: memberIds emitted. Size: ${it.size}") } + + // Observe the device info of the users in the room + val cryptoDeviceInfoObservable = memberIdsChangeObservable + .switchMap { membersIds -> + session.getLiveCryptoDeviceInfo(membersIds) + .asObservable() + .map { + // If any key change, emit the userIds list + membersIds + } + .startWith(membersIds) + .doOnNext { Timber.v("RX: CryptoDeviceInfo emitted. Size: ${it.size}") } + } + .doOnNext { Timber.v("RX: cryptoDeviceInfo emitted 2. Size: ${it.size}") } + + val roomEncryptionTrustLevelObservable = cryptoDeviceInfoObservable + .map { userIds -> + if (userIds.isEmpty()) { + Optional(null) + } else { + session.getCrossSigningService().getTrustLevelForUsers(userIds).toOptional() + } + } + .doOnNext { Timber.v("RX: roomEncryptionTrustLevel emitted: ${it.getOrNull()?.name}") } + + return Observable + .combineLatest, Optional, Optional>( + summaryObservable, + roomEncryptionTrustLevelObservable, + BiFunction { summary, level -> + summary.getOrNull()?.copy( + roomEncryptionTrustLevel = level.getOrNull() + ).toOptional() + } + ) + .doOnNext { Timber.v("RX: final room summary emitted for ${it.getOrNull()?.roomId}") } } fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { - return room.getRoomMembersLive(queryParams).asObservable() + val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable() .startWithCallable { room.getRoomMembers(queryParams) } + .doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") } + + // TODO Do it only for room members of the room (switchMap) + val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable() + .startWith(emptyList()) + .doOnNext { Timber.v("RX: cryptoDeviceInfo emitted. Size: ${it.size}") } + + return Observable + .combineLatest, List, List>( + roomMembersObservable, + cryptoDeviceInfoObservable, + BiFunction { summaries, _ -> + summaries.map { + if (room.isEncrypted()) { + it.copy( + // Get the trust level of a virtual room with only this user + userEncryptionTrustLevel = session.getCrossSigningService().getTrustLevelForUsers(listOf(it.userId)) + ) + } else { + it + } + } + } + ) + .doOnNext { Timber.v("RX: final room members emitted. Size: ${it.size}") } } fun liveAnnotationSummary(eventId: String): Observable> { @@ -98,6 +180,6 @@ class RxRoom(private val room: Room) { } } -fun Room.rx(): RxRoom { - return RxRoom(this) +fun Room.rx(session: Session): RxRoom { + return RxRoom(this, session) } 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 edba4ca9bf..31e1834d2b 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,16 +30,43 @@ 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 +import io.reactivex.functions.BiFunction +import timber.log.Timber class RxSession(private val session: Session) { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable> { - return session.getRoomSummariesLive(queryParams).asObservable() + val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable() .startWithCallable { session.getRoomSummaries(queryParams) } + .doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") } + + val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable() + .startWith(emptyList()) + .doOnNext { Timber.v("RX: crypto device info emitted: size: ${it.size}") } + + return Observable + .combineLatest, List, List>( + summariesObservable, + cryptoDeviceInfoObservable, + BiFunction { summaries, _ -> + summaries.map { + if (it.isEncrypted) { + it.copy( + roomEncryptionTrustLevel = session.getCrossSigningService() + .getTrustLevelForUsers(it.otherMemberIds + session.myUserId) + ) + } else { + it + } + } + } + ) + .doOnNext { Timber.d("RX: final summaries emitted: size: ${it.size}") } } fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable> { @@ -106,6 +134,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/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt index f2fbde3fe7..d8f24ee416 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt @@ -18,5 +18,8 @@ package im.vector.matrix.android import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors -internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main) +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, + Executors.newSingleThreadExecutor().asCoroutineDispatcher()) 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 f348a8b75c..8489e5a7cf 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,8 +32,16 @@ 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 org.junit.Assert.* -import java.util.* +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.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import java.util.ArrayList +import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -73,23 +82,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) } /** @@ -276,7 +287,7 @@ class CommonTestHelper(context: Context) { fun signout(session: Session) { val lock = CountDownLatch(1) - session.signOut(true, object : TestMatrixCallback(lock) {}) + session.signOut(true, TestMatrixCallback(lock)) 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..59db3b287c 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,8 +33,16 @@ 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 org.junit.Assert.* -import java.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import java.util.Arrays +import java.util.HashMap import java.util.concurrent.CountDownLatch class CryptoTestHelper(val mTestHelper: CommonTestHelper) { @@ -49,7 +61,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { var roomId: String? = null val lock1 = CountDownLatch(1) - aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, object : TestMatrixCallback(lock1) { + aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), object : TestMatrixCallback(lock1) { override fun onSuccess(data: String) { roomId = data super.onSuccess(data) @@ -78,26 +90,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 +125,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..c8e7355d7a --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -0,0 +1,209 @@ +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.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.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 latch = CountDownLatch(2) + + aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(latch)) + bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(latch)) + + mTestHelper.await(latch) + + // 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) + + assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted()) + + 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 latch = CountDownLatch(2) + + aliceSession.getCrossSigningService().initializeCrossSigning(aliceAuthParams, TestMatrixCallback(latch)) + bobSession.getCrossSigningService().initializeCrossSigning(bobAuthParams, TestMatrixCallback(latch)) + + mTestHelper.await(latch) + + // 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..312ad03a06 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,11 @@ 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 +1178,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..6ae2489993 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 @@ -19,22 +19,34 @@ package im.vector.matrix.android.internal.crypto.verification import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest 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.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasMode +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.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 org.junit.Assert.* +import im.vector.matrix.android.internal.crypto.model.rest.toValue +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.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @@ -50,53 +62,57 @@ 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, + null) 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) { + val immutableState = (tx as SASDefaultVerificationTransaction).state + if (immutableState is VerificationTxState.Cancelled && !immutableState.byMe) { cancelLatch.countDown() } } @@ -104,29 +120,32 @@ 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) - assertEquals("Should be cancelled on bob side", - SasVerificationTxState.OnCancelled, bobSasTx.state) + assertTrue("Should be cancelled on alice side", aliceSasTx.state is VerificationTxState.Cancelled) + assertTrue("Should be cancelled on bob side", bobSasTx.state is VerificationTxState.Cancelled) - assertEquals("Should be User cancelled on alice side", - CancelCode.User, aliceSasTx.cancelledReason) - assertEquals("Should be User cancelled on bob side", - CancelCode.User, aliceSasTx.cancelledReason) + val aliceCancelState = aliceSasTx.state as VerificationTxState.Cancelled + val bobCancelState = bobSasTx.state as VerificationTxState.Cancelled - assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)) - assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID)) + assertTrue("Should be cancelled by me on alice side", aliceCancelState.byMe) + assertFalse("Should be cancelled by other on bob side", bobCancelState.byMe) + + assertEquals("Should be User cancelled on alice side", CancelCode.User, aliceCancelState.cancelCode) + assertEquals("Should be User cancelled on bob side", CancelCode.User, bobCancelState.cancelCode) + + assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)) + assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID)) cryptoTestData.close() } @Test fun test_key_agreement_protocols_must_include_curve25519() { + fail("Not passing for the moment") val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val bobSession = cryptoTestData.secondSession!! @@ -135,8 +154,23 @@ class SASTest : InstrumentedTest { val tid = "00000000" // Bob should receive a cancel - var canceledToDeviceEvent: Event? = null + var cancelReason: CancelCode? = 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.state is VerificationTxState.Cancelled) { + cancelReason = (tx.state as VerificationTxState.Cancelled).cancelCode + 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,31 +186,31 @@ 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, cancelReason) cryptoTestData.close() } @Test fun test_key_agreement_macs_Must_include_hmac_sha256() { + fail("Not passing for the moment") val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val bobSession = cryptoTestData.secondSession!! @@ -214,6 +248,7 @@ class SASTest : InstrumentedTest { @Test fun test_key_agreement_short_code_include_decimal() { + fail("Not passing for the moment") val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val bobSession = cryptoTestData.secondSession!! @@ -253,18 +288,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 +323,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 = mutableListOf() + 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 is VerificationTxState.Cancelled && !(tx.state as VerificationTxState.Cancelled).byMe) { 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, null) + aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId, null) mTestHelper.await(aliceCreatedLatch) mTestHelper.await(aliceCancelledLatch) @@ -329,46 +365,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, null) mTestHelper.await(aliceAcceptedLatch) assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) @@ -393,38 +429,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 +469,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, null) 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 +493,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 +533,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, null) 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/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/RoomEncryptionTrustLevel.kt new file mode 100644 index 0000000000..ee431ae23a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/RoomEncryptionTrustLevel.kt @@ -0,0 +1,31 @@ +/* + * 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.crypto + +/** + * RoomEncryptionTrustLevel represents the trust level in an encrypted room. + */ +enum class RoomEncryptionTrustLevel { + // No one in the room has been verified -> Black shield + Default, + + // There are one or more device un-verified -> the app should display a red shield + Warning, + + // All devices in the room are verified -> the app should display a green shield + Trusted +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Booleans.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Booleans.kt new file mode 100644 index 0000000000..f6bf7d5f7d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Booleans.kt @@ -0,0 +1,21 @@ +/* + * 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.extensions + +fun Boolean?.orTrue() = this ?: true + +fun Boolean?.orFalse() = this ?: false 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/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index a3b5ce39eb..5dfb0eab9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -26,3 +26,8 @@ fun Throwable.is401() = fun Throwable.isTokenError() = this is Failure.ServerError && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) + +fun Throwable.shouldBeRetried(): Boolean { + return this is Failure.NetworkConnection + || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) +} 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..46539d9029 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,15 @@ 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(): LiveData> + + fun getLiveCryptoDeviceInfo(userId: String): LiveData> + + fun getLiveCryptoDeviceInfo(userIds: List): 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..a9d7a9e241 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.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.matrix.android.api.session.crypto.crosssigning + +import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel +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 isCrossSigningVerified(): 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 + + fun getTrustLevelForUsers(userIds: List): RoomEncryptionTrustLevel +} 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..8d75fe2a91 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/MXCrossSigningInfo.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.crosssigning + +import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey +import im.vector.matrix.android.internal.crypto.model.KeyUsage + +data class MXCrossSigningInfo( + val userId: String, + val crossSigningKeys: List +) { + + 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..ef6462f854 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/QrCodeVerificationTransaction.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.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) + + /** + * Call when you confirm that other user has scanned your QR code + */ + fun otherUserScannedMyQrCode() + + /** + * Call when you do not confirm that other user has scanned your QR code + */ + fun otherUserDidNotScannedMyQrCode() +} 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/SasVerificationService.kt deleted file mode 100644 index 88c0787b4d..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ /dev/null @@ -1,39 +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.matrix.android.api.session.crypto.sas - -interface SasVerificationService { - fun addListener(listener: SasVerificationListener) - - fun removeListener(listener: SasVerificationListener) - - fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) - - fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? - - fun beginKeyVerificationSAS(userId: String, deviceID: String): String? - - fun beginKeyVerification(method: String, userId: String, deviceID: String): String? - - // fun transactionUpdated(tx: SasVerificationTransaction) - - interface SasVerificationListener { - fun transactionCreated(tx: SasVerificationTransaction) - fun transactionUpdated(tx: SasVerificationTransaction) - fun markedAsManuallyVerified(userId: String, deviceId: 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 d24ccadb55..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 { - val state: SasVerificationTxState - - val cancelledReason: CancelCode? - - val transactionId: String - - val otherUserId: String - - var otherDeviceId: String? - - val isIncoming: Boolean +interface SasVerificationTransaction : VerificationTransaction { fun supportsEmoji(): Boolean @@ -37,14 +26,11 @@ 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 */ fun userHasVerifiedShortCode() + + fun shortCodeDoesNotMatch() } 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/SasVerificationTxState.kt deleted file mode 100644 index 350ec2c381..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt +++ /dev/null @@ -1,49 +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.matrix.android.api.session.crypto.sas - -enum class SasVerificationTxState { - None, - // I have started a verification request - SendingStart, - Started, - // Other user/device sent me a request - OnStarted, - // I have accepted a request started by the other user/device - SendingAccept, - Accepted, - // My request has been accepted by the other user/device - OnAccepted, - // I have sent my public key - SendingKey, - KeySent, - // The other user/device has sent me his public key - OnKeyReceived, - // Short code is ready to be displayed - ShortCodeReady, - // I have compared the code and manually said that they match - ShortCodeAccepted, - - SendingMac, - MacSent, - Verifying, - Verified, - - // Global: The verification has been cancelled (by me or other), see cancelReason for details - Cancelled, - OnCancelled -} 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 new file mode 100644 index 0000000000..b8f0f23891 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.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.api.session.crypto.sas + +/** + * Verification methods + */ +enum class VerificationMethod { + // Use it when your application supports the SAS verification method + SAS, + // 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/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt new file mode 100644 index 0000000000..0dd143f792 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt @@ -0,0 +1,122 @@ +/* + * 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.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. + * 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 VerificationService { + + fun addListener(listener: VerificationListener) + + fun removeListener(listener: VerificationListener) + + /** + * Mark this device as verified manually + */ + fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + + fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? + + fun getExistingVerificationRequest(otherUserId: String): List? + + fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? + + fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? + + fun beginKeyVerification(method: VerificationMethod, + otherUserId: String, + otherDeviceId: String, + transactionId: String?): String? + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest + + /** + * Request a key verification from another user using toDevice events. + */ + fun requestKeyVerification(methods: List, + otherUserId: String, + otherDevices: List?): 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, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback?): String? + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerificationInDMs(methods: List, + otherUserId: String, + roomId: String, + transactionId: String): Boolean + + /** + * Returns false if the request is unknown + */ + fun readyPendingVerification(methods: List, + otherUserId: String, + transactionId: String): Boolean + + // fun transactionUpdated(tx: SasVerificationTransaction) + + interface VerificationListener { + fun transactionCreated(tx: VerificationTransaction) + fun transactionUpdated(tx: VerificationTransaction) + fun markedAsManuallyVerified(userId: String, deviceId: String) {} + + fun verificationRequestCreated(pr: PendingVerificationRequest) {} + fun verificationRequestUpdated(pr: PendingVerificationRequest) {} + } + + companion object { + + private const val TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000 + private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 + + fun isValidRequest(age: Long?): Boolean { + if (age == null) return false + val now = System.currentTimeMillis() + val tooInThePast = now - TEN_MINUTES_IN_MILLIS + val tooInTheFuture = now + FIVE_MINUTES_IN_MILLIS + return age in tooInThePast..tooInTheFuture + } + } +} 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..6ed650b2ad --- /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 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/VerificationTxState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt new file mode 100644 index 0000000000..b30dde2d4d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationTxState.kt @@ -0,0 +1,54 @@ +/* + * 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.api.session.crypto.sas + +sealed class VerificationTxState { + // Uninitialized state + object None : VerificationTxState() + + // Specific for SAS + abstract class VerificationSasTxState : VerificationTxState() + + object SendingStart : VerificationSasTxState() + object Started : VerificationSasTxState() + object OnStarted : VerificationSasTxState() + object SendingAccept : VerificationSasTxState() + object Accepted : VerificationSasTxState() + object OnAccepted : VerificationSasTxState() + object SendingKey : VerificationSasTxState() + object KeySent : VerificationSasTxState() + object OnKeyReceived : VerificationSasTxState() + object ShortCodeReady : VerificationSasTxState() + object ShortCodeAccepted : VerificationSasTxState() + object SendingMac : VerificationSasTxState() + object MacSent : VerificationSasTxState() + object Verifying : VerificationSasTxState() + + // Specific for QR code + abstract class VerificationQrTxState : VerificationTxState() + + // Will be used to ask the user if the other user has correctly scanned + object QrScannedByOther : VerificationQrTxState() + + // Terminal states + abstract class TerminalTxState : VerificationTxState() + + object Verified : TerminalTxState() + + // Cancelled by me or by other + data class Cancelled(val cancelCode: CancelCode, val byMe: Boolean) : TerminalTxState() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index bc6885eddc..553c36a9f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -85,6 +85,14 @@ data class Event( @Transient var sendState: SendState = SendState.UNKNOWN + /** + * The `age` value transcoded in a timestamp based on the device clock when the SDK received + * the event from the home server. + * Unlike `age`, this value is static. + */ + @Transient + var ageLocalTs: Long? = null + /** * Check if event is a state event. * @return true if event is state event. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 224b4262ab..8878930de0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -72,6 +72,8 @@ object EventType { const val KEY_VERIFICATION_KEY = "m.key.verification.key" const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" + const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" // Relation Events const val REACTION = "m.reaction" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index 57eaa7dc76..b179cb7a31 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -21,9 +21,23 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class UnsignedData( + /** + * The time in milliseconds that has elapsed since the event was sent. + * This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers + * is out of sync, which can cause the age to either be negative or greater than it actually is. + */ @Json(name = "age") val age: Long?, + /** + * Optional. The event that redacted this event, if any. + */ @Json(name = "redacted_because") val redactedEvent: Event? = null, + /** + * The client-supplied transaction ID, if the client being given the event is the same one which sent it. + */ @Json(name = "transaction_id") val transactionId: String? = null, + /** + * Optional. The previous content for this event. If there is no previous content, this key will be missing. + */ @Json(name = "prev_content") val prevContent: Map? = null, @Json(name = "m.relations") val relations: AggregatedRelations? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index f3167c8461..9fec605bd9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -101,4 +101,6 @@ interface RoomService { fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable + + fun getExistingDirectRoomWithUser(otherUserId: String) : Room? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt index 0d403be2f4..28edfcfe04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt @@ -18,5 +18,6 @@ package im.vector.matrix.android.api.session.room.model data class EventAnnotationsSummary( var eventId: String, var reactionsSummary: List, - var editSummary: EditAggregatedSummary? + var editSummary: EditAggregatedSummary?, + var referencesAggregatedSummary: ReferencesAggregatedSummary? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt new file mode 100644 index 0000000000..ae6e52a091 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt @@ -0,0 +1,31 @@ +/* + * 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.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Contains an aggregated summary info of the references. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class ReferencesAggregatedContent( + // Verification status info for m.key.verification.request msgType events + @Json(name = "verif_sum") val verificationSummary: String + // Add more fields for future summary info. +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt new file mode 100644 index 0000000000..018436ad6b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt @@ -0,0 +1,29 @@ +/* + * 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.api.session.room.model + +import im.vector.matrix.android.api.session.events.model.Content + +/** + * Events can relates to other events, this object keeps a summary + * of all events that are referencing the 'eventId' event via the RelationType.REFERENCE + */ +data class ReferencesAggregatedSummary( + val eventId: String, + val content: Content?, + val sourceEvents: List, + val localEchos: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt index 17768362b2..d3b7f3f026 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt @@ -16,12 +16,16 @@ package im.vector.matrix.android.api.session.room.model +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel + /** * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ -data class RoomMemberSummary( +data class RoomMemberSummary constructor( val membership: Membership, val userId: String, val displayName: String? = null, - val avatarUrl: String? = null + val avatarUrl: String? = null, + // TODO Warning: Will not be populated if not using RxRoom + val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 7ec6254613..28c56125f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.session.room.model +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -24,7 +25,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent * This class holds some data of a room. * It can be retrieved by [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] */ -data class RoomSummary( +data class RoomSummary constructor( val roomId: String, val displayName: String = "", val topic: String = "", @@ -46,7 +47,9 @@ data class RoomSummary( val userDrafts: List = emptyList(), var isEncrypted: Boolean, val typingRoomMemberIds: List = emptyList(), - val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS + val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, + // TODO Plug it + val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null ) { val isVersioned: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 2119c586db..b69c189f89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -35,95 +35,111 @@ import timber.log.Timber * Parameter to create a room, with facilities functions to configure it */ @JsonClass(generateAdapter = true) -class CreateRoomParams { +data class CreateRoomParams( + /** + * A public visibility indicates that the room will be shown in the published room list. + * A private visibility will hide the room from the published room list. + * Rooms default to private visibility if this key is not included. + * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] + */ + @Json(name = "visibility") + val visibility: RoomDirectoryVisibility? = null, - /** - * A public visibility indicates that the room will be shown in the published room list. - * A private visibility will hide the room from the published room list. - * Rooms default to private visibility if this key is not included. - * NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"] - */ - var visibility: RoomDirectoryVisibility? = null + /** + * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. + * The alias will belong on the same homeserver which created the room. + * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. + */ + @Json(name = "room_alias_name") + val roomAliasName: String? = null, - /** - * The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room. - * The alias will belong on the same homeserver which created the room. - * For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com. - */ - @Json(name = "room_alias_name") - var roomAliasName: String? = null + /** + * If this is included, an m.room.name event will be sent into the room to indicate the name of the room. + * See Room Events for more information on m.room.name. + */ + @Json(name = "name") + val name: String? = null, - /** - * If this is included, an m.room.name event will be sent into the room to indicate the name of the room. - * See Room Events for more information on m.room.name. - */ - var name: String? = null + /** + * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room. + * See Room Events for more information on m.room.topic. + */ + @Json(name = "topic") + val topic: String? = null, - /** - * If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room. - * See Room Events for more information on m.room.topic. - */ - var topic: String? = null + /** + * A list of user IDs to invite to the room. + * This will tell the server to invite everyone in the list to the newly created room. + */ + @Json(name = "invite") + val invitedUserIds: List? = null, - /** - * A list of user IDs to invite to the room. - * This will tell the server to invite everyone in the list to the newly created room. - */ - @Json(name = "invite") - var invitedUserIds: MutableList? = null + /** + * A list of objects representing third party IDs to invite into the room. + */ + @Json(name = "invite_3pid") + val invite3pids: List? = null, - /** - * A list of objects representing third party IDs to invite into the room. - */ - @Json(name = "invite_3pid") - var invite3pids: MutableList? = null + /** + * Extra keys to be added to the content of the m.room.create. + * The server will clobber the following keys: creator. + * Future versions of the specification may allow the server to clobber other keys. + */ + @Json(name = "creation_content") + val creationContent: Any? = null, - /** - * Extra keys to be added to the content of the m.room.create. - * The server will clobber the following keys: creator. - * Future versions of the specification may allow the server to clobber other keys. - */ - @Json(name = "creation_content") - var creationContent: Any? = null + /** + * A list of state events to set in the new room. + * This allows the user to override the default state events set in the new room. + * The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by presets, but gets overridden by name and topic keys. + */ + @Json(name = "initial_state") + val initialStates: List? = null, - /** - * A list of state events to set in the new room. - * This allows the user to override the default state events set in the new room. - * The expected format of the state events are an object with type, state_key and content keys set. - * Takes precedence over events set by presets, but gets overridden by name and topic keys. - */ - @Json(name = "initial_state") - var initialStates: MutableList? = null + /** + * Convenience parameter for setting various default state events based on a preset. Must be either: + * private_chat => join_rules is set to invite. history_visibility is set to shared. + * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the + * room creator. + * public_chat: => join_rules is set to public. history_visibility is set to shared. + */ + @Json(name = "preset") + val preset: CreateRoomPreset? = null, - /** - * Convenience parameter for setting various default state events based on a preset. Must be either: - * private_chat => join_rules is set to invite. history_visibility is set to shared. - * trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the - * room creator. - * public_chat: => join_rules is set to public. history_visibility is set to shared. - */ - var preset: CreateRoomPreset? = null + /** + * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. + * See Direct Messaging for more information. + */ + @Json(name = "is_direct") + val isDirect: Boolean? = null, + /** + * The power level content to override in the default power level event + */ + @Json(name = "power_level_content_override") + val powerLevelContentOverride: PowerLevelsContent? = null +) { /** - * This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid. - * See Direct Messaging for more information. + * Set to true means that if cross-signing is enabled and we can get keys for every invited users, + * the encryption will be enabled on the created room */ - @Json(name = "is_direct") - var isDirect: Boolean? = null + @Transient + internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false + private set - /** - * The power level content to override in the default power level event - */ - @Json(name = "power_level_content_override") - var powerLevelContentOverride: PowerLevelsContent? = null + fun enableEncryptionIfInvitedUsersSupportIt(): CreateRoomParams { + enableEncryptionIfInvitedUsersSupportIt = true + return this + } /** * Add the crypto algorithm to the room creation parameters. * * @param algorithm the algorithm */ - fun enableEncryptionWithAlgorithm(algorithm: String) { - if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { + fun enableEncryptionWithAlgorithm(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams { + return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) { val contentMap = mapOf("algorithm" to algorithm) val algoEvent = Event( @@ -132,13 +148,12 @@ class CreateRoomParams { content = contentMap.toContent() ) - if (null == initialStates) { - initialStates = mutableListOf(algoEvent) - } else { - initialStates!!.add(algoEvent) - } + copy( + initialStates = initialStates.orEmpty().filter { it.type != EventType.STATE_ROOM_ENCRYPTION } + algoEvent + ) } else { Timber.e("Unsupported algorithm: $algorithm") + this } } @@ -147,9 +162,10 @@ class CreateRoomParams { * * @param historyVisibility the expected history visibility, set null to remove any existing value. */ - fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) { + fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams { // Remove the existing value if any. - initialStates?.removeAll { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY } + val newInitialStates = initialStates + ?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY } if (historyVisibility != null) { val contentMap = mapOf("history_visibility" to historyVisibility) @@ -159,20 +175,24 @@ class CreateRoomParams { stateKey = "", content = contentMap.toContent()) - if (null == initialStates) { - initialStates = mutableListOf(historyVisibilityEvent) - } else { - initialStates!!.add(historyVisibilityEvent) - } + return copy( + initialStates = newInitialStates.orEmpty() + historyVisibilityEvent + ) + } else { + return copy( + initialStates = newInitialStates + ) } } /** * Mark as a direct message room. */ - fun setDirectMessage() { - preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT - isDirect = true + fun setDirectMessage(): CreateRoomParams { + return copy( + preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT, + isDirect = true + ) } /** @@ -215,28 +235,26 @@ class CreateRoomParams { */ fun addParticipantIds(hsConfig: HomeServerConnectionConfig, userId: String, - ids: List) { - for (id in ids) { - if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) { - if (null == invite3pids) { - invite3pids = ArrayList() - } - val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!, - medium = ThreePidMedium.EMAIL, - address = id) - - invite3pids!!.add(pid) - } else if (isUserId(id)) { - // do not invite oneself - if (userId != id) { - if (null == invitedUserIds) { - invitedUserIds = ArrayList() - } - - invitedUserIds!!.add(id) - } - } - // TODO add phonenumbers when it will be available - } + ids: List): CreateRoomParams { + return copy( + invite3pids = (invite3pids.orEmpty() + ids + .takeIf { hsConfig.identityServerUri != null } + ?.filter { id -> Patterns.EMAIL_ADDRESS.matcher(id).matches() } + ?.map { id -> + Invite3Pid( + idServer = hsConfig.identityServerUri!!.host!!, + medium = ThreePidMedium.EMAIL, + address = id + ) + } + .orEmpty()) + .distinct(), + invitedUserIds = (invitedUserIds.orEmpty() + ids + .filter { id -> isUserId(id) } + // do not invite oneself + .filter { id -> id != userId }) + .distinct() + ) + // TODO add phonenumbers when it will be available } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index 157ff00d69..c0adfa4c36 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent interface MessageContent { + // TODO Rename to msgType val type: String val body: String val relatesTo: RelationDefaultContent? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt new file mode 100644 index 0000000000..f65215e2bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt @@ -0,0 +1,26 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +data class MessageRelationContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index 8cef40f21a..d4e6d5ea71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -26,6 +26,7 @@ object MessageType { const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" + const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt new file mode 100644 index 0000000000..d4fb3a44fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt @@ -0,0 +1,76 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +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.verification.VerificationInfoAccept +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory +import timber.log.Timber + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationAcceptContent( + @Json(name = "hash") override val hash: String?, + @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, + @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "commitment") override var commitment: String? = null +) : VerificationInfoAccept { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() + || keyAgreementProtocol.isNullOrBlank() + || hash.isNullOrBlank() + || commitment.isNullOrBlank() + || messageAuthenticationCode.isNullOrBlank() + || shortAuthenticationStrings.isNullOrEmpty()) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + override fun toEventContent() = toContent() + + companion object : VerificationInfoAcceptFactory { + + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept { + return MessageVerificationAcceptContent( + hash, + keyAgreementProtocol, + messageAuthenticationCode, + shortAuthenticationStrings, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ), + commitment + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt new file mode 100644 index 0000000000..651a25e175 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -0,0 +1,58 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.events.model.RelationType +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.verification.VerificationInfoCancel + +@JsonClass(generateAdapter = true) +data class MessageVerificationCancelContent( + @Json(name = "code") override val code: String? = null, + @Json(name = "reason") override val reason: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? + +) : VerificationInfoCancel { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { + return false + } + return true + } + + companion object { + fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { + return MessageVerificationCancelContent( + reason.value, + reason.humanReadable, + RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt new file mode 100644 index 0000000000..224c5c52d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -0,0 +1,36 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +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.verification.VerificationInfo + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationDoneContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfo { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid() = transactionID?.isNotEmpty() == true + + override fun toEventContent(): Content? = toContent() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt new file mode 100644 index 0000000000..777e0e7318 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt @@ -0,0 +1,61 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +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.verification.VerificationInfoKey +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory +import timber.log.Timber + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationKeyContent( + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoKey { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + override fun toEventContent() = toContent() + + companion object : VerificationInfoKeyFactory { + + override fun create(tid: String, pubKey: String): VerificationInfoKey { + return MessageVerificationKeyContent( + pubKey, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt new file mode 100644 index 0000000000..05e614dee9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt @@ -0,0 +1,57 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +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.verification.VerificationInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMacFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationMacContent( + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "keys") override val keys: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoMac { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { + return false + } + return true + } + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { + return MessageVerificationMacContent( + mac, + keys, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt new file mode 100644 index 0000000000..4e237ba1c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -0,0 +1,57 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +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.verification.MessageVerificationReadyFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationReadyContent( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "methods") override val methods: List? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoReady { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) { + return false + } + return true + } + + companion object : MessageVerificationReadyFactory { + override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + methods = methods, + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt new file mode 100644 index 0000000000..c8026e2f26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +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.verification.VerificationInfoRequest + +@JsonClass(generateAdapter = true) +data class MessageVerificationRequestContent( + @Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = "body") override val body: String, + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List, + @Json(name = "to") val toUserId: String, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "format") val format: String? = null, + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent, VerificationInfoRequest { + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) { + return false + } + return true + } + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() +} 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 new file mode 100644 index 0000000000..3031b213d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt @@ -0,0 +1,84 @@ +/* + * 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.api.session.room.model.message + +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.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.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 + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationStartContent( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "hashes") override val hashes: List?, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, + @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 = "secret") override val sharedSecret: String? +) : VerificationInfoStart { + + override fun toCanonicalJson(): String? { + return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) + } + + override val transactionID: String? + get() = relatesTo?.eventId + + // TODO Move those method to the interface? + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() + || fromDevice.isNullOrBlank() + || (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 b728750acc..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 @@ -180,6 +240,12 @@ internal abstract class CryptoModule { @Binds abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask + @Binds + abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask + + @Binds + abstract fun bindSendVerificationMessageTask(sendDefaultSendVerificationMessageTask: DefaultSendVerificationMessageTask): SendVerificationMessageTask + @Binds abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice) : ClaimOneTimeKeysForUsersDeviceTask @@ -187,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 7c6a40e3bb..c5c587fd9b 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,6 +58,7 @@ 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.DeleteDeviceTask @@ -62,7 +67,7 @@ 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.DefaultSasVerificationService +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.model.EventEntityFields import im.vector.matrix.android.internal.database.query.where @@ -123,8 +128,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, // @@ -150,6 +157,10 @@ internal class DefaultCryptoService @Inject constructor( private val cryptoCoroutineScope: CoroutineScope ) : CryptoService { + init { + verificationService.cryptoService = this + } + private val uiHandler = Handler(Looper.getMainLooper()) // MXEncrypting instance for each room. @@ -176,7 +187,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) } @@ -201,7 +222,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 } @@ -321,9 +342,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 @@ -357,7 +380,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 @@ -370,13 +393,28 @@ 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(): LiveData> { + return cryptoStore.getLiveDeviceList() + } + + override fun getLiveCryptoDeviceInfo(userId: String): LiveData> { + return cryptoStore.getLiveDeviceList(userId) + } + + override fun getLiveCryptoDeviceInfo(userIds: List): LiveData> { + return cryptoStore.getLiveDeviceList(userIds) + } /** * Set the devices as known @@ -401,7 +439,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 } } @@ -418,12 +456,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) } /** @@ -504,9 +542,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 { @@ -768,11 +805,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) } @@ -1012,8 +1053,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 -> @@ -1028,7 +1069,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..87b2093e2d 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,7 +19,9 @@ 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 @@ -36,6 +38,36 @@ 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 = mutableListOf() + + fun addListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.add(listener) + } + } + + fun removeListener(listener: UserDevicesUpdateListener) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.remove(listener) + } + } + + private fun dispatchDeviceChange(users: List) { + synchronized(deviceChangeListeners) { + deviceChangeListeners.forEach { + try { + it.onUsersDeviceUpdate(users) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to dispatch device change") + } + } + } + } + // HS not ready for retry private val notReadyToRetryHS = mutableSetOf() @@ -166,13 +198,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 +214,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 +239,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } } cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + + dispatchDeviceChange(userIds) return usersDevicesInfoMap } @@ -217,10 +251,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 +299,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: List): 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 +317,63 @@ 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) } + + + Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models") + if (!models.isNullOrEmpty()) { + val workingCopy = models.toMutableMap() + 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) + workingCopy.remove(deviceId) if (null != previouslyStoredDeviceKeys) { // But keep old validated ones if any - mutableDevices[deviceId] = previouslyStoredDeviceKeys + workingCopy[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 + workingCopy[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, workingCopy) } + + // 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 +387,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 +415,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..814d9d5a7c 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") @@ -122,6 +122,14 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( continue } } + + // If cross signing is available on account we automatically discard untrust devices request + if (cryptoStore.getMyCrossSigningInfo() != null) { + // At this point the device is unknown, we don't want to bother user with that + cryptoStore.deleteIncomingRoomKeyRequest(request) + continue + } + cryptoStore.storeIncomingRoomKeyRequest(request) onRoomKeyRequest(request) } 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..9121ce3fcb 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 @@ -21,7 +21,12 @@ import im.vector.matrix.android.api.session.crypto.MXCryptoError 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.internal.crypto.* +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.NewSessionListener +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting @@ -59,7 +64,10 @@ internal class MXMegolmDecryption(private val userId: String, private var pendingEvents: MutableMap>> = HashMap() override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - return decryptEvent(event, timeline, true) + // If cross signing is enabled, we don't send request until the keys are trusted + // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once + val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true + return decryptEvent(event, timeline, requestOnFail) } private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { @@ -297,7 +305,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..7dec3236a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -0,0 +1,690 @@ +/* + * 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.crypto.RoomEncryptionTrustLevel +import im.vector.matrix.android.api.extensions.orFalse +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.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? + 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) + setUserKeysAsTrusted(userId, true) + cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding()) + + uploadSigningKeysTask.configureWith(params) { + 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.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() { + masterPkSigning?.releaseSigning() + userPkSigning?.releaseSigning() + selfSigningPkSigning?.releaseSigning() + + masterPkSigning = null + userPkSigning = null + 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 isCrossSigningVerified(): 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()}") + 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()) + } + + if (otherUserId == userId) { + // It's me, i should check if a newly trusted device is signing my master key + // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust + setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) + } + } + } + + private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { + val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() + cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) + + // If it's me, recheck trust of all users and devices? + val users = ArrayList() + if (otherUserId == userId && currentTrust != trusted) { + cryptoStore.updateUsersTrust { + users.add(it) + checkUserTrust(it).isVerified() + } + + users.forEach { + cryptoStore.getUserDeviceList(it)?.forEach { device -> + val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) + Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) + } + } + } + } + + override fun getTrustLevelForUsers(userIds: List): RoomEncryptionTrustLevel { + val allTrusted = userIds + .filter { getUserCrossSigningKeys(it)?.isTrusted() == true } + + val allUsersAreVerified = userIds.size == allTrusted.size + + return if (allTrusted.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // Green if all devices of all verified users are trusted -> green + // else black + val allDevices = allTrusted.mapNotNull { + cryptoStore.getUserDeviceList(it) + }.flatten() + if (getMyCrossSigningKeys() != null) { + val hasWarning = allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default + } + } else { + val hasWarningLegacy = allDevices.any { !it.isVerified } + if (hasWarningLegacy) { + RoomEncryptionTrustLevel.Warning + } else { + if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default + } + } + } + } +} 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..075b141f8d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustLevel.kt @@ -0,0 +1,25 @@ +/* + * 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..27d2c1e76d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DeviceTrustResult.kt @@ -0,0 +1,31 @@ +/* + * 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() = this is DeviceTrustResult.Success +fun DeviceTrustResult.isCrossSignedVerified() = this is DeviceTrustResult.Success && level.isCrossSigningVerified() +fun DeviceTrustResult.isLocallyVerified() = this is 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..c4235531fb --- /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..b12eaabaed --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoCrossSigningKey.kt @@ -0,0 +1,108 @@ +/* + * 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 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") +} + +internal 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..8f6d64221c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -0,0 +1,99 @@ +/* + * 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!! +// ) +// } +} + +internal 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..4459d508ff --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt @@ -0,0 +1,80 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.model.rest.RestDeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.RestKeyInfo + +internal object CryptoInfoMapper { + + 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..fa4daa112c 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 + val 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..563e0499ae 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,24 @@ 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/KeyChangesResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt index 5a48f547f9..12d27a023f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass * This class describes the key changes response */ @JsonClass(generateAdapter = true) -data class KeyChangesResponse( +internal data class KeyChangesResponse( // list of user ids which have new devices @Json(name = "changed") var changed: List? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt index 7be6f2042c..dc6075ca43 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -17,13 +17,15 @@ 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.verification.VerificationInfoAccept +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory import timber.log.Timber /** * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. */ @JsonClass(generateAdapter = true) -data class KeyVerificationAccept( +internal data class KeyVerificationAccept( /** * string to identify the transaction. @@ -31,39 +33,41 @@ data class KeyVerificationAccept( * Alice’s device should record this ID and use it in future messages in this transaction. */ @Json(name = "transaction_id") - var transactionID: String? = null, + override val transactionID: String? = null, /** * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device */ @Json(name = "key_agreement_protocol") - var keyAgreementProtocol: String? = null, + override val keyAgreementProtocol: String? = null, /** * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device */ - var hash: String? = null, + @Json(name = "hash") + override val hash: String? = null, /** * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device */ @Json(name = "message_authentication_code") - var messageAuthenticationCode: String? = null, + override val messageAuthenticationCode: String? = null, /** * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device */ @Json(name = "short_authentication_string") - var shortAuthenticationStrings: List? = null, + override val shortAuthenticationStrings: List? = null, /** * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) * and the canonical JSON representation of the m.key.verification.start message. */ - var commitment: String? = null -) : SendToDeviceObject { + @Json(name = "commitment") + override var commitment: String? = null +) : SendToDeviceObject, VerificationInfoAccept { - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keyAgreementProtocol.isNullOrBlank() || hash.isNullOrBlank() @@ -76,21 +80,23 @@ data class KeyVerificationAccept( return true } - companion object { - fun create(tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List): KeyVerificationAccept { - return KeyVerificationAccept().apply { - this.transactionID = tid - this.keyAgreementProtocol = keyAgreementProtocol - this.hash = hash - this.commitment = commitment - this.messageAuthenticationCode = messageAuthenticationCode - this.shortAuthenticationStrings = shortAuthenticationStrings - } + override fun toSendToDeviceObject() = this + + companion object : VerificationInfoAcceptFactory { + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept { + return KeyVerificationAccept( + transactionID = tid, + keyAgreementProtocol = keyAgreementProtocol, + hash = hash, + commitment = commitment, + messageAuthenticationCode = messageAuthenticationCode, + shortAuthenticationStrings = shortAuthenticationStrings + ) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt index b5c45e9566..d1ae09cc37 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -18,40 +18,43 @@ 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.CancelCode +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel /** * To device event sent by either party to cancel a key verification. */ @JsonClass(generateAdapter = true) -data class KeyVerificationCancel( +internal data class KeyVerificationCancel( /** * the transaction ID of the verification to cancel */ @Json(name = "transaction_id") - var transactionID: String? = null, + override val transactionID: String? = null, /** * machine-readable reason for cancelling, see #CancelCode */ - var code: String? = null, + override val code: String? = null, /** * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. */ - var reason: String? = null -) : SendToDeviceObject { + override val reason: String? = null +) : SendToDeviceObject, VerificationInfoCancel { companion object { fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { - return KeyVerificationCancel().apply { - this.transactionID = tid - this.code = cancelCode.value - this.reason = cancelCode.humanReadable - } + return KeyVerificationCancel( + tid, + cancelCode.value, + cancelCode.humanReadable + ) } } - fun isValid(): Boolean { + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { return false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.kt new file mode 100644 index 0000000000..c0a72d29db --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationDone.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.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoDone + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationDone( + @Json(name = "transaction_id") override var transactionID: String? = null +) : SendToDeviceObject, VerificationInfoDone { + + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank()) { + return false + } + return true + } +} 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 4c6243fee3..9a190e1e15 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 @@ -17,37 +17,35 @@ 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.verification.VerificationInfoKeyFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey /** * Sent by both devices to send their ephemeral Curve25519 public key to the other device. */ @JsonClass(generateAdapter = true) -data class KeyVerificationKey( +internal data class KeyVerificationKey( /** * the ID of the transaction that the message is part of */ - @Json(name = "transaction_id") - @JvmField - var transactionID: String? = null, + @Json(name = "transaction_id") override val transactionID: String? = null, /** * The device’s ephemeral public key, as an unpadded base64 string */ - @JvmField - var key: String? = null + @Json(name = "key") override val key: String? = null -) : SendToDeviceObject { +) : SendToDeviceObject, VerificationInfoKey { - companion object { - fun create(tid: String, key: String): KeyVerificationKey { - return KeyVerificationKey().apply { - this.transactionID = tid - this.key = key - } + companion object : VerificationInfoKeyFactory { + override fun create(tid: String, pubKey: String): KeyVerificationKey { + return KeyVerificationKey(tid, pubKey) } } - fun isValid(): Boolean { + 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 8732e366d2..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 @@ -17,49 +17,32 @@ 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.verification.VerificationInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMacFactory /** * Sent by both devices to send the MAC of their device key to the other device. */ @JsonClass(generateAdapter = true) -data class KeyVerificationMac( - /** - * the ID of the transaction that the message is part of - */ - @Json(name = "transaction_id") - var transactionID: String? = null, +internal data class KeyVerificationMac( + @Json(name = "transaction_id") override val transactionID: String? = null, + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "keys") override val keys: String? = null - /** - * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key - */ - @JvmField - var mac: Map? = null, +) : SendToDeviceObject, VerificationInfoMac { - /** - * The MAC of the comma-separated, sorted list of key IDs given in the mac property, - * as an unpadded base64 string, calculated using the MAC key. - * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will - * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. - */ - @JvmField - var keys: String? = null - -) : SendToDeviceObject { - - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { return false } return true } - companion object { - fun create(tid: String, mac: Map, keys: String): KeyVerificationMac { - return KeyVerificationMac().apply { - this.transactionID = tid - this.mac = mac - this.keys = keys - } + override fun toSendToDeviceObject(): SendToDeviceObject? = this + + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { + return KeyVerificationMac(tid, mac, keys) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt new file mode 100644 index 0000000000..004477e90b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt @@ -0,0 +1,37 @@ +/* + * 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.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationReady( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List?, + @Json(name = "transaction_id") override val transactionID: String? = null +) : SendToDeviceObject, VerificationInfoReady { + + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { + return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt new file mode 100644 index 0000000000..5bd09658b5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -0,0 +1,42 @@ +/* + * 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.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoRequest + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationRequest( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "methods") override val methods: List, + @Json(name = "timestamp") override val timestamp: Long?, + @Json(name = "transaction_id") override var transactionID: String? = null + +) : SendToDeviceObject, VerificationInfoRequest { + + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) { + return false + } + return true + } +} 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 081b19161a..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,81 +18,64 @@ 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 /** * Sent by Alice to initiate an interactive key verification. */ @JsonClass(generateAdapter = true) -class KeyVerificationStart : SendToDeviceObject { +internal data class KeyVerificationStart( + @Json(name = "from_device") override val fromDevice: String? = null, + override val method: String? = null, + @Json(name = "transaction_id") override val transactionID: String? = null, + @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, + // For QR code verification + @Json(name = "secret") override val sharedSecret: String? = null +) : SendToDeviceObject, VerificationInfoStart { - /** - * Alice’s device ID - */ - @Json(name = "from_device") - var fromDevice: String? = null - - var method: String? = null - - /** - * String to identify the transaction. - * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. - * Alice’s device should record this ID and use it in future messages in this transaction. - */ - @Json(name = "transaction_id") - var transactionID: String? = null - - /** - * An array of key agreement protocols that Alice’s client understands. - * Must include “curve25519”. - * Other methods may be defined in the future - */ - @Json(name = "key_agreement_protocols") - var keyAgreementProtocols: List? = null - - /** - * An array of hashes that Alice’s client understands. - * Must include “sha256”. Other methods may be defined in the future. - */ - var hashes: List? = null - - /** - * An array of message authentication codes that Alice’s client understands. - * Must include “hkdf-hmac-sha256”. - * Other methods may be defined in the future. - */ - @Json(name = "message_authentication_codes") - var messageAuthenticationCodes: List? = null - - /** - * An array of short authentication string methods that Alice’s client (and Alice) understands. - * Must include “decimal”. - * This document also describes the “emoji” method. - * Other methods may be defined in the future - */ - @Json(name = "short_authentication_string") - var shortAuthenticationStrings: List? = null - - companion object { - const val VERIF_METHOD_SAS = "m.sas.v1" + override fun toCanonicalJson(): String? { + return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) } - fun isValid(): Boolean { + // TODO Move those method to the interface? + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() - || method != VERIF_METHOD_SAS - || keyAgreementProtocols.isNullOrEmpty() - || hashes.isNullOrEmpty() - || hashes?.contains("sha256") == false - || messageAuthenticationCodes.isNullOrEmpty() - || (messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256) == false - && messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF) == false) - || shortAuthenticationStrings.isNullOrEmpty() - || shortAuthenticationStrings?.contains(SasMode.DECIMAL) == false) { + || (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/KeysClaimBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt index 87b9891ffa..38f6615dad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt @@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. */ @JsonClass(generateAdapter = true) -data class KeysClaimBody( +internal data class KeysClaimBody( /** * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt index 976743d8bc..59567ba77a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt @@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass * This class represents the response to /keys/claim request made by claimOneTimeKeysForUsersDevices. */ @JsonClass(generateAdapter = true) -data class KeysClaimResponse( +internal data class KeysClaimResponse( /** * The requested keys ordered by device by user. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt index 769a28f7e7..3dca696fcd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt @@ -24,7 +24,7 @@ import com.squareup.moshi.JsonClass * This class represents the body to /keys/query */ @JsonClass(generateAdapter = true) -data class KeysQueryBody( +internal data class KeysQueryBody( /** * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. 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..dd3cb049dd 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,24 +18,37 @@ 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( +internal data class KeysQueryResponse( /** * The device keys per devices per users. * Map from userId to map from deviceId to MXDeviceInfo * TODO Use MXUsersDevicesMap? */ @Json(name = "device_keys") - var deviceKeys: Map>? = null, + val deviceKeys: Map>? = null, /** * The failures sorted by homeservers. TODO Bad comment ? * TODO Use MXUsersDevicesMap? */ - var failures: Map>? = null + val failures: Map>? = null, + + @Json(name = "master_keys") + val masterKeys: Map? = null, + + @Json(name = "self_signing_keys") + val selfSigningKeys: Map? = null, + + @Json(name = "user_signing_keys") + val 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..2267c25534 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 @@ -21,10 +21,10 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.util.JsonDict @JsonClass(generateAdapter = true) -data class KeysUploadBody( +internal data class KeysUploadBody( @Json(name = "device_keys") - var deviceKeys: DeviceKeys? = null, + val deviceKeys: RestDeviceInfo? = null, @Json(name = "one_time_keys") - var oneTimeKeys: JsonDict? = null + val oneTimeKeys: JsonDict? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt index b68c13bf4d..38360fa1cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass * This class represents the response to /keys/upload request made by uploadKeys. */ @JsonClass(generateAdapter = true) -data class KeysUploadResponse( +internal data class KeysUploadResponse( /** * The count per algorithm as returned by the home server: a map (algorithm to count). 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..1fc1e599b7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RestDeviceInfo.kt @@ -0,0 +1,60 @@ +/* + * 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) +internal data class RestDeviceInfo( + /** + * The id of this device. + */ + @Json(name = "device_id") + val deviceId: String, + + /** + * the user id + */ + @Json(name = "user_id") + val userId: String, + + /** + * The list of algorithms supported by this device. + */ + @Json(name = "algorithms") + val algorithms: List? = null, + + /** + * A map from ":" to "". + */ + @Json(name = "keys") + val keys: Map? = null, + + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + @Json(name = "signatures") + val signatures: Map>? = null, + + /* + * Additional data from the home server. + */ + @Json(name = "unsigned") + val 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..5c4ff4d7e4 --- /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) +internal 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/RoomKeyShare.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt index 00ad490e1d..de2345e002 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.Json * Parent class representing an room key action request * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] */ -open class RoomKeyShare : SendToDeviceObject { +internal open class RoomKeyShare : SendToDeviceObject { var action: String? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt index 768c2d71c5..fcfbfccbac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass * Class representing an room key request cancellation content */ @JsonClass(generateAdapter = true) -class RoomKeyShareCancellation : RoomKeyShare() { +internal class RoomKeyShareCancellation : RoomKeyShare() { init { action = ACTION_SHARE_CANCELLATION } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt index 0d8f7f8738..3b9d210812 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -22,8 +22,7 @@ import com.squareup.moshi.JsonClass * Class representing an room key request content */ @JsonClass(generateAdapter = true) -class RoomKeyShareRequest : RoomKeyShare() { - +internal class RoomKeyShareRequest : RoomKeyShare() { var body: RoomKeyRequestBody? = null init { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt index 8b95d7c686..df4482f0bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt @@ -16,13 +16,12 @@ package im.vector.matrix.android.internal.crypto.model.rest -class SendToDeviceBody { - - /** - * `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization - * - * The messages to send. A map from user ID, to a map from device ID to message body. - * The device ID may also be *, meaning all known devices for the user. - */ - var messages: Map>? = null -} +internal data class SendToDeviceBody( + /** + * `Any` should implement [SendToDeviceObject], but we cannot use interface here because of Json serialization + * + * The messages to send. A map from user ID, to a map from device ID to message body. + * The device ID may also be *, meaning all known devices for the user. + */ + val messages: Map>? +) 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..ef459fbc59 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SignatureUploadResponse.kt @@ -0,0 +1,49 @@ +/* + * 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) +internal 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. + */ + val 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/UpdateDeviceInfoBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt index 161c5d0354..f2ea24a960 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt @@ -20,12 +20,10 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class UpdateDeviceInfoBody( - +internal data class UpdateDeviceInfoBody( /** * The new display name for this device. If not given, the display name is unchanged. */ @Json(name = "display_name") var displayName: String? = null - ) 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..05d2d88f95 --- /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 + */ +internal data class UploadSignatureQueryBuilder( + private val deviceInfoList: MutableList = mutableListOf(), + private val signingKeyInfoList: MutableList = mutableListOf() +) { + + 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 78% 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..45ad43a0ef 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,24 +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.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") - var session: String? = null, + val session: String? = null, // registration information @Json(name = "type") - var type: String? = null, + val type: String? = LoginFlowTypes.PASSWORD, @Json(name = "user") - var user: String? = null, + val user: String? = null, @Json(name = "password") - var password: String? = null + val password: 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 new file mode 100644 index 0000000000..643ac5a495 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.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.model.rest + +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod + +internal const val VERIFICATION_METHOD_SAS = "m.sas.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.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW + } +} 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..3a12df2cd7 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,16 @@ 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> + + fun getLiveDeviceList(userIds: List): LiveData> + + // TODO temp + fun getLiveDeviceList(): LiveData> /** * Store the crypto algorithm for a room. * @@ -381,4 +397,28 @@ 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() + + fun updateUsersTrust(check: (String) -> Boolean) } 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..51adc46900 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,176 @@ 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) + } + + // Only for me + if (userSigningKey != null) { + val existingUSK = signingInfo.getUserSigningKey() + if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { + // update signatures? + existingUSK.putSignatures(userSigningKey.signatures) + existingUSK.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + } else { + val keyEntity = realm.createObject(KeyInfoEntity::class.java).apply { + this.publicKeyBase64 = userSigningKey.unpaddedBase64PublicKey + this.usages = userSigningKey.usages?.toTypedArray()?.let { RealmList(*it) } + ?: RealmList() + this.putSignatures(userSigningKey.signatures) + } + signingInfo.setUserSignedKey(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 getLiveDeviceList(userIds: List): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + .`in`(UserEntityFields.USER_ID, userIds.toTypedArray()) + }, + { entity -> + entity.devices.map { CryptoMapper.mapToModel(it) } + } + ) + return Transformations.map(liveData) { + it.firstOrNull() ?: emptyList() + } + } + + override fun getLiveDeviceList(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { 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 +917,205 @@ 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 updateUsersTrust(check: (String) -> Boolean) { + doRealmTransaction(realmConfiguration) { realm -> + val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) + .findAll() + xInfoEntities?.forEach { xInfoEntity -> + // Need to ignore mine + if (xInfoEntity.userId == credentials.userId) return@forEach + val mapped = mapCrossSigningInfoEntity(xInfoEntity) + val currentTrust = mapped.isTrusted() + val newTrust = check(mapped.userId) + if (currentTrust != newTrust) { + xInfoEntity.crossSigningKeys.forEach { info -> + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = newTrust + newLevel.crossSignedVerified = newTrust + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = newTrust + level.crossSignedVerified = newTrust + } + } + } + } + } + } + + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { + return doRealmQueryAndCopy(realmConfiguration) { realm -> + realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + }?.let { xsignInfo -> + mapCrossSigningInfoEntity(xsignInfo) + } + } + + private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + return MXCrossSigningInfo( + userId = xsignInfo.userId ?: "", + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + val pubKey = it.publicKeyBase64 ?: return@mapNotNull null + CryptoCrossSigningKey( + userId = xsignInfo.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..a967f8e002 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CrossSigningInfoEntity.kt @@ -0,0 +1,58 @@ +/* + * 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) } + } + + fun getUserSigningKey() = crossSigningKeys.firstOrNull { it.usages.contains(KeyUsage.USER_SIGNING.value) } + + fun setUserSignedKey(info: KeyInfoEntity?) { + crossSigningKeys + .filter { it.usages.contains(KeyUsage.USER_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..98f931a455 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,25 @@ 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) - : RealmObject() { + // 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..e2c0242193 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/TrustLevelEntity.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.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..940fa9c7fb 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,8 +18,8 @@ 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.DeleteDeviceParams +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task @@ -42,16 +42,16 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params) { return executeRequest(eventBus) { - apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams() - .apply { - deleteDeviceAuth = DeleteDeviceAuth() - .apply { - type = LoginFlowTypes.PASSWORD - session = params.authSession - user = userId + apiCall = cryptoApi.deleteDevice(params.deviceId, + DeleteDeviceParams( + userPasswordAuth = UserPasswordAuth( + type = LoginFlowTypes.PASSWORD, + session = params.authSession, + user = userId, password = params.password - } - }) + ) + ) + ) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt new file mode 100644 index 0000000000..951bc6385a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt @@ -0,0 +1,80 @@ +/* + * 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.session.crypto.CryptoService +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult +import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitCallback +import javax.inject.Inject + +internal interface EncryptEventTask : Task { + data class Params(val roomId: String, + val event: Event, + /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + val keepKeys: List? = null, + val crypto: CryptoService + ) +} + +internal class DefaultEncryptEventTask @Inject constructor( +// private val crypto: CryptoService + private val localEchoUpdater: LocalEchoUpdater +) : EncryptEventTask { + override suspend fun execute(params: EncryptEventTask.Params): Event { + if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event + val localEvent = params.event + if (localEvent.eventId == null) { + throw IllegalArgumentException() + } + + localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + +// try { + awaitCallback { + params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + }.let { result -> + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + return localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + } +// } catch (throwable: Throwable) { +// val sendState = when (throwable) { +// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES +// else -> SendState.UNDELIVERED +// } +// localEchoUpdater.updateSendState(localEvent.eventId, sendState) +// throw throwable +// } + } +} 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 new file mode 100644 index 0000000000..5a8f8e7ba5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -0,0 +1,154 @@ +/* + * 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.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +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.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 +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +internal interface RoomVerificationUpdateTask : Task { + data class Params( + val events: List, + val verificationService: DefaultVerificationService, + val cryptoService: CryptoService + ) +} + +internal class DefaultRoomVerificationUpdateTask @Inject constructor( + @UserId private val userId: String, + @DeviceId private val deviceId: String?, + private val cryptoService: CryptoService) : RoomVerificationUpdateTask { + + companion object { + // XXX what about multi-account? + private val transactionsHandledByOtherDevice = ArrayList() + } + + override suspend fun execute(params: RoomVerificationUpdateTask.Params) { + // TODO ignore initial sync or back pagination? + + params.events.forEach { event -> + Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + Timber.v("## SAS Verification live observer: received msgId: $event") + + // 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 (!VerificationService.isValidRequest(event.ageLocalTs + ?: event.originServerTs)) return@forEach Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + } + + // decrypt if needed? + if (event.isEncrypted() && event.mxDecryptionResult == null) { + // TODO use a global event decryptor? attache to session and that listen to new sessionId? + // for now decrypt sync + try { + val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + } + } + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") + + if (event.senderId == userId) { + // If it's send from me, we need to keep track of Requests or Start + // done from another device of mine + + if (EventType.MESSAGE == event.type) { + val msgType = event.getClearContent().toModel()?.type + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is requested from another device + Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ") + event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + } + } + } + } else if (EventType.KEY_VERIFICATION_START == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + params.verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_READY == event.type) { + event.getClearContent().toModel()?.let { + if (it.fromDevice != deviceId) { + // The verification is started from another device + Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ") + it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) } + params.verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) { + event.getClearContent().toModel()?.relatesTo?.eventId?.let { + transactionsHandledByOtherDevice.remove(it) + params.verificationService.onRoomRequestHandledByOtherDevice(event) + } + } + + Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") + return@forEach + } + + val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId + if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) { + // Ignore this event, it is directed to another of my devices + Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ") + return@forEach + } + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_DONE -> { + params.verificationService.onRoomEvent(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + params.verificationService.onRoomRequestReceived(event) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt index 2b4ab74f87..58c461888b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt @@ -42,8 +42,9 @@ internal class DefaultSendToDeviceTask @Inject constructor( ) : SendToDeviceTask { override suspend fun execute(params: SendToDeviceTask.Params) { - val sendToDeviceBody = SendToDeviceBody() - sendToDeviceBody.messages = params.contentMap.map + val sendToDeviceBody = SendToDeviceBody( + messages = params.contentMap.map + ) return executeRequest(eventBus) { apiCall = cryptoApi.sendToDevice( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt new file mode 100644 index 0000000000..da72d34ba4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -0,0 +1,79 @@ +/* + * 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.session.crypto.CryptoService +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater +import im.vector.matrix.android.internal.session.room.send.SendResponse +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface SendVerificationMessageTask : Task { + data class Params( + val event: Event, + val cryptoService: CryptoService? + ) +} + +internal class DefaultSendVerificationMessageTask @Inject constructor( + private val localEchoUpdater: LocalEchoUpdater, + private val encryptEventTask: DefaultEncryptEventTask, + private val roomAPI: RoomAPI, + private val eventBus: EventBus) : SendVerificationMessageTask { + + override suspend fun execute(params: SendVerificationMessageTask.Params): String { + val event = handleEncryption(params) + val localID = event.eventId!! + + try { + localEchoUpdater.updateSendState(localID, SendState.SENDING) + val executeRequest = executeRequest(eventBus) { + apiCall = roomAPI.send( + localID, + roomId = event.roomId ?: "", + content = event.content, + eventType = event.type + ) + } + localEchoUpdater.updateSendState(localID, SendState.SENT) + return executeRequest.eventId + } catch (e: Throwable) { + localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { + if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.event.roomId ?: "", + params.event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return params.event + } +} 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..af097f4431 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). @@ -45,15 +45,10 @@ internal class DefaultUploadKeysTask @Inject constructor( override suspend fun execute(params: UploadKeysTask.Params): KeysUploadResponse { val encodedDeviceId = convertToUTF8(params.deviceId) - val body = KeysUploadBody() - - if (null != params.deviceKeys) { - body.deviceKeys = params.deviceKeys - } - - if (null != params.oneTimeKeys) { - body.oneTimeKeys = params.oneTimeKeys - } + val body = KeysUploadBody( + deviceKeys = params.deviceKeys, + oneTimeKeys = params.oneTimeKeys + ) return executeRequest(eventBus) { apiCall = if (encodedDeviceId.isBlank()) { 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..f102479339 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadSignaturesTask.kt @@ -0,0 +1,52 @@ +/* + * 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) { + this.isRetryable = true + this.maxRetryCount = 10 + this.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/IncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt similarity index 57% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 6ed5be4881..efaa67d05f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -16,83 +16,85 @@ package im.vector.matrix.android.internal.crypto.verification import android.util.Base64 -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.BuildConfig +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.model.rest.KeyVerificationAccept -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.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber -internal class IncomingSASVerificationTransaction( - private val sasVerificationService: DefaultSasVerificationService, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, +internal class DefaultIncomingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val userId: String, + override val deviceId: String?, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, + crossSigningService: CrossSigningService, deviceFingerprint: String, transactionId: String, - otherUserID: String) - : SASVerificationTransaction( - sasVerificationService, + otherUserID: String, + private val autoAccept: Boolean = false +) : SASDefaultVerificationTransaction( setDeviceVerificationAction, - credentials, + userId, + deviceId, cryptoStore, - sendToDeviceTask, - taskExecutor, + 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 + return when (val immutableState = state) { + is VerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT + is VerificationTxState.SendingAccept, + is VerificationTxState.Accepted, + is VerificationTxState.OnKeyReceived, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } else { + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } + } else -> IncomingSasVerificationTransaction.UxState.UNKNOWN } } - override fun onVerificationStart(startReq: KeyVerificationStart) { - Timber.v("## SAS received verification request from state $state") - if (state != SasVerificationTxState.None) { - Timber.e("## received verification request from invalid state") + override fun onVerificationStart(startReq: VerificationInfoStart) { + Timber.v("## SAS I: received verification request from state $state") + 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) { + performAccept() + } } override fun performAccept() { - if (state != SasVerificationTxState.OnStarted) { - Timber.e("## Cannot perform accept from state $state") + if (state != VerificationTxState.OnStarted) { + Timber.e("## SAS Cannot perform accept from state $state") return } @@ -109,24 +111,24 @@ internal class IncomingSASVerificationTransaction( if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } || agreedShortCode.isNullOrEmpty()) { // Failed to find agreement - Timber.e("## Failed to find agreement ") + Timber.e("## SAS Failed to find agreement ") cancel(CancelCode.UnknownMethod) return } // 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("## Failed to find device key ") + Timber.e("## SAS Failed to find device key ") // TODO force download keys!! // would be probably better to download the keys // for now I cancel cancel(CancelCode.User) } else { - // val otherKey = info.identityKey() + // val otherKey = info.identityKey() // need to jump back to correct thread - val accept = KeyVerificationAccept.create( + val accept = transport.createAccept( tid = transactionId, keyAgreementProtocol = agreedProtocol!!, hash = agreedHash!!, @@ -138,33 +140,33 @@ internal class IncomingSASVerificationTransaction( } } - private fun doAccept(accept: KeyVerificationAccept) { + private fun doAccept(accept: VerificationInfoAccept) { this.accepted = accept - Timber.v("## SAS accept request id:$transactionId") + Timber.v("## SAS incoming accept request id:$transactionId") // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, // concatenated with the canonical JSON representation of the content of the m.key.verification.start message - val concat = getSAS().publicKey + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) + 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 } } } - override fun onVerificationAccept(accept: KeyVerificationAccept) { + override fun onVerificationAccept(accept: VerificationInfoAccept) { Timber.v("## SAS invalid message for incoming request id:$transactionId") cancel(CancelCode.UnexpectedMessage) } - override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { + override fun onKeyVerificationKey(vKey: VerificationInfoKey) { Timber.v("## SAS received key for request id:$transactionId") - if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { - Timber.e("## received key from invalid state $state") + if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { + Timber.e("## SAS received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } @@ -175,13 +177,13 @@ internal class IncomingSASVerificationTransaction( // sending Bob’s public key QB val pubKey = getSAS().publicKey - val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) + 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 } } @@ -198,30 +200,29 @@ internal class IncomingSASVerificationTransaction( // - 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) - Timber.e("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") - Timber.e("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + if (BuildConfig.LOG_PRIVATE_DATA) { + Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") + Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + } - state = SasVerificationTxState.ShortCodeReady + state = VerificationTxState.ShortCodeReady } - override fun onKeyVerificationMac(vKey: KeyVerificationMac) { - Timber.v("## SAS received mac for request id:$transactionId") + 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) { - Timber.e("## received key from invalid state $state") + 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/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt new file mode 100644 index 0000000000..4befb6aff3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -0,0 +1,230 @@ +/* + * 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.verification + +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.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.store.IMXCryptoStore +import timber.log.Timber + +internal class DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + userId: String, + deviceId: String?, + cryptoStore: IMXCryptoStore, + crossSigningService: CrossSigningService, + deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String +) : SASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + deviceFingerprint, + transactionId, + otherUserId, + otherDeviceId, + isIncoming = false), + OutgoingSasVerificationTransaction { + + override val uxState: OutgoingSasVerificationTransaction.UxState + get() { + return when (val immutableState = state) { + is VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START + is VerificationTxState.SendingStart, + is VerificationTxState.Started, + is VerificationTxState.OnAccepted, + is VerificationTxState.SendingKey, + is VerificationTxState.KeySent, + is VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + is VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS + is VerificationTxState.ShortCodeAccepted, + is VerificationTxState.SendingMac, + is VerificationTxState.MacSent, + is VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + is VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED + is VerificationTxState.Cancelled -> { + if (immutableState.byMe) { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + } else { + OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME + } + } + else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: VerificationInfoStart) { + Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + 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.createStartForSas( + deviceId ?: "", + transactionId, + KNOWN_AGREEMENT_PROTOCOLS, + KNOWN_HASHES, + KNOWN_MACS, + KNOWN_SHORT_CODES + ) + + startReq = startMessage + state = VerificationTxState.SendingStart + + sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + VerificationTxState.Started, + CancelCode.User, + null + ) + } + +// fun request() { +// if (state != VerificationTxState.None) { +// Timber.e("## start verification from invalid state") +// // should I cancel?? +// throw IllegalStateException("Interactive Key verification already started") +// } +// +// val requestMessage = KeyVerificationRequest( +// fromDevice = session.sessionParams.credentials.deviceId ?: "", +// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), +// timestamp = System.currentTimeMillis().toInt(), +// transactionID = transactionId +// ) +// +// sendToOther( +// EventType.KEY_VERIFICATION_REQUEST, +// requestMessage, +// VerificationTxState.None, +// CancelCode.User, +// null +// ) +// } + + override fun onVerificationAccept(accept: VerificationInfoAccept) { + Timber.v("## SAS O: onVerificationAccept id:$transactionId") + if (state != VerificationTxState.Started) { + Timber.e("## SAS O: received accept request from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + // Check that the agreement is correct + if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) + || !KNOWN_HASHES.contains(accept.hash) + || !KNOWN_MACS.contains(accept.messageAuthenticationCode) + || accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { + Timber.e("## SAS O: received accept request from invalid state") + cancel(CancelCode.UnknownMethod) + return + } + + // 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 = 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 + val pubKey = getSAS().publicKey + + val keyToDevice = transport.createKey(transactionId, pubKey) + // we need to send this to other device now + 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 == VerificationTxState.SendingKey) { + state = VerificationTxState.KeySent + } + } + } + + override fun onKeyVerificationKey(vKey: VerificationInfoKey) { + Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") + if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Bob’s device, + // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept + // message is the same as the expected value based on the value of the key property received + // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. + + // check commitment + val concat = vKey.key + startReq!!.toCanonicalJson() + val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" + + if (accepted!!.commitment.equals(otherCommitment)) { + getSAS().setTheirPublicKey(otherKey) + // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - 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$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 = VerificationTxState.ShortCodeReady + } else { + // bad commitment + cancel(CancelCode.MismatchedCommitment) + } + } + + override fun onKeyVerificationMac(vKey: VerificationInfoMac) { + Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") + 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 + } + + theirMac = vKey + + // Do I have my Mac? + if (myMac != null) { + // I can check + verifyMacs() + } + // Wait for ShortCode Accepted + } +} 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/DefaultSasVerificationService.kt deleted file mode 100644 index e0cd47e0e0..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ /dev/null @@ -1,413 +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.matrix.android.internal.crypto.verification - -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.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState -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.toModel -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.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.* -import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import timber.log.Timber -import java.lang.Exception -import java.util.UUID -import javax.inject.Inject -import kotlin.collections.HashMap - -/** - * Manages all current verifications transactions with short codes. - * Short codes interactive verification is a more user friendly way of verifying devices - * that is still maintaining a good level of security (alternative to the 43-character strings compare method). - */ - -@SessionScope -internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val myDeviceInfoHolder: Lazy, - private val deviceListManager: DeviceListManager, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor) - : VerificationTransaction.Listener, SasVerificationService { - - private val uiHandler = Handler(Looper.getMainLooper()) - - // map [sender : [transaction]] - private val txMap = HashMap>() - - // Event received from the sync - fun onToDeviceEvent(event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { - when (event.getClearType()) { - EventType.KEY_VERIFICATION_START -> { - onStartRequestReceived(event) - } - EventType.KEY_VERIFICATION_CANCEL -> { - onCancelReceived(event) - } - EventType.KEY_VERIFICATION_ACCEPT -> { - onAcceptReceived(event) - } - EventType.KEY_VERIFICATION_KEY -> { - onKeyReceived(event) - } - EventType.KEY_VERIFICATION_MAC -> { - onMacReceived(event) - } - else -> { - // ignore - } - } - } - } - - private var listeners = ArrayList() - - override fun addListener(listener: SasVerificationService.SasVerificationListener) { - uiHandler.post { - if (!listeners.contains(listener)) { - listeners.add(listener) - } - } - } - - override fun removeListener(listener: SasVerificationService.SasVerificationListener) { - uiHandler.post { - listeners.remove(listener) - } - } - - private fun dispatchTxAdded(tx: VerificationTransaction) { - uiHandler.post { - listeners.forEach { - try { - it.transactionCreated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - private fun dispatchTxUpdated(tx: VerificationTransaction) { - uiHandler.post { - listeners.forEach { - try { - it.transactionUpdated(tx) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - } - - override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { - setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, - deviceID, - userId) - - listeners.forEach { - try { - it.markedAsManuallyVerified(userId, deviceID) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") - } - } - } - - private suspend fun onStartRequestReceived(event: Event) { - val startReq = event.getClearContent().toModel()!! - - val otherUserId = event.senderId - if (!startReq.isValid()) { - Timber.e("## received invalid verification request") - if (startReq.transactionID != null) { - cancelTransaction( - startReq.transactionID!!, - otherUserId!!, - startReq.fromDevice ?: event.getSenderKey()!!, - CancelCode.UnknownMethod - ) - } - return - } - // Download device keys prior to everything - if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { - Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}") - 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) - 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) - } - cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } else { - // Ok we can create - if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { - Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") - val tx = IncomingSASVerificationTransaction( - this, - setDeviceVerificationAction, - credentials, - cryptoStore, - sendToDeviceTask, - taskExecutor, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - startReq.transactionID!!, - otherUserId) - addTransaction(tx) - tx.acceptToDeviceEvent(otherUserId, startReq) - } else { - Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") - cancelTransaction(tid, otherUserId, startReq.fromDevice - ?: event.getSenderKey()!!, CancelCode.UnknownMethod) - } - } - } else { - cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) - } - } - - private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: KeyVerificationStart): MXUsersDevicesMap? { - return try { - val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) - val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null - keys.takeIf { deviceIds.contains(startReq.fromDevice) } - } catch (e: Exception) { - null - } - } - - private suspend fun onCancelReceived(event: Event) { - Timber.v("## SAS onCancelReceived") - val cancelReq = event.getClearContent().toModel()!! - - if (!cancelReq.isValid()) { - // ignore - Timber.e("## Received invalid accept request") - return - } - val otherUserId = event.senderId!! - - Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") - val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!) - if (existing == null) { - Timber.e("## Received invalid cancel request") - return - } - if (existing is SASVerificationTransaction) { - existing.cancelledReason = safeValueOf(cancelReq.code) - existing.state = SasVerificationTxState.OnCancelled - } - } - - private suspend fun onAcceptReceived(event: Event) { - val acceptReq = event.getClearContent().toModel()!! - - if (!acceptReq.isValid()) { - // ignore - Timber.e("## Received invalid accept request") - return - } - val otherUserId = event.senderId!! - val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) - if (existing == null) { - Timber.e("## Received invalid accept request") - return - } - - if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, acceptReq) - } else { - // not other types now - } - } - - private suspend fun onKeyReceived(event: Event) { - val keyReq = event.getClearContent().toModel()!! - - if (!keyReq.isValid()) { - // ignore - Timber.e("## Received invalid key request") - return - } - val otherUserId = event.senderId!! - val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) - if (existing == null) { - Timber.e("## Received invalid accept request") - return - } - if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, keyReq) - } else { - // not other types now - } - } - - private suspend fun onMacReceived(event: Event) { - val macReq = event.getClearContent().toModel()!! - - if (!macReq.isValid()) { - // ignore - Timber.e("## Received invalid key request") - return - } - val otherUserId = event.senderId!! - val existing = getExistingTransaction(otherUserId, macReq.transactionID!!) - if (existing == null) { - Timber.e("## Received invalid accept request") - return - } - if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, macReq) - } else { - // not other types known for now - } - } - - override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? { - synchronized(lock = txMap) { - return txMap[otherUser]?.get(tid) - } - } - - private fun getExistingTransactionsForUser(otherUser: String): Collection? { - synchronized(txMap) { - return txMap[otherUser]?.values - } - } - - private fun removeTransaction(otherUser: String, tid: String) { - synchronized(txMap) { - txMap[otherUser]?.remove(tid)?.removeListener(this) - } - } - - private fun addTransaction(tx: VerificationTransaction) { - tx.otherUserId.let { otherUserId -> - synchronized(txMap) { - val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } - txInnerMap[tx.transactionId] = tx - dispatchTxAdded(tx) - tx.addListener(this) - } - } - } - - override fun beginKeyVerificationSAS(userId: String, deviceID: String): String? { - return beginKeyVerification(KeyVerificationStart.VERIF_METHOD_SAS, userId, deviceID) - } - - override fun beginKeyVerification(method: String, userId: String, deviceID: String): String? { - val txID = createUniqueIDForTransaction(userId, deviceID) - // should check if already one (and cancel it) - if (KeyVerificationStart.VERIF_METHOD_SAS == method) { - val tx = OutgoingSASVerificationRequest( - this, - setDeviceVerificationAction, - credentials, - cryptoStore, - sendToDeviceTask, - taskExecutor, - myDeviceInfoHolder.get().myDevice.fingerprint()!!, - txID, - userId, - deviceID) - addTransaction(tx) - - tx.start() - return txID - } else { - throw IllegalArgumentException("Unknown verification method") - } - } - - /** - * 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 { - return buildString { - append(credentials.userId).append("|") - append(credentials.deviceId).append("|") - append(userId).append("|") - append(deviceID).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) - ) { - // remove - this.removeTransaction(tx.otherUserId, tx.transactionId) - } - } - - fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { - val cancelMessage = KeyVerificationCancel.create(transactionId, code) - val contentMap = MXUsersDevicesMap() - contentMap.setObject(userId, userDevice, cancelMessage) - - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") - } - - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") - } - } - } - .executeBy(taskExecutor) - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt new file mode 100644 index 0000000000..911fc2a572 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -0,0 +1,1365 @@ +/* + * 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.verification + +import android.os.Handler +import android.os.Looper +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.crypto.CryptoService +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.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.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.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.KeyVerificationRequest +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.UUID +import javax.inject.Inject +import kotlin.collections.set + +@SessionScope +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 verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, + private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, + private val crossSigningService: CrossSigningService +) : DefaultVerificationTransaction.Listener, VerificationService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + // Cannot be injected in constructor as it creates a dependency cycle + lateinit var cryptoService: CryptoService + + // map [sender : [transaction]] + private val txMap = HashMap>() + + /** + * Map [sender: [PendingVerificationRequest]] + * For now we keep all requests (even terminated ones) during the lifetime of the app. + */ + private val pendingRequests = HashMap>() + + // Event received from the sync + fun onToDeviceEvent(event: Event) { + GlobalScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + onCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onKeyReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onReadyReceived(event) + } + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + onRequestReceived(event) + } + else -> { + // ignore + } + } + } + } + + fun onRoomEvent(event: Event) { + GlobalScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onRoomStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device + onRoomCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onRoomAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onRoomKeyRequestReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onRoomMacReceived(event) + } + EventType.KEY_VERIFICATION_READY -> { + onRoomReadyReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + onRoomDoneReceived(event) + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + onRoomRequestReceived(event) + } + } + else -> { + // ignore + } + } + } + } + + private var listeners = ArrayList() + + override fun addListener(listener: VerificationService.VerificationListener) { + uiHandler.post { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + } + + override fun removeListener(listener: VerificationService.VerificationListener) { + uiHandler.post { + listeners.remove(listener) + } + } + + private fun dispatchTxAdded(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchTxUpdated(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + userId, + deviceID) + + listeners.forEach { + try { + it.markedAsManuallyVerified(userId, deviceID) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + + fun onRoomRequestHandledByOtherDevice(event: Event) { + val requestInfo = event.getClearContent().toModel() + ?: return + val requestId = requestInfo.relatesTo?.eventId ?: return + getExistingVerificationRequestInRoom(event.roomId ?: "", requestId)?.let { + updatePendingRequest( + it.copy( + handledByOtherSession = true + ) + ) + } + } + + private fun onRequestReceived(event: Event) { + val requestInfo = event.getClearContent().toModel()!! + + if (!requestInfo.isValid()) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + val senderId = event.senderId ?: return + + // We don't want to block here + val otherDeviceId = requestInfo.fromDevice ?: return + + GlobalScope.launch { + if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { + Timber.e("## Verification device $otherDeviceId is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests[senderId] + ?: ArrayList().also { + pendingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = null, + transactionId = requestInfo.transactionID, + localID = requestInfo.transactionID!!, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + } + + suspend fun onRoomRequestReceived(event: Event) { + Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + val requestInfo = event.getClearContent().toModel() + ?: return + val senderId = event.senderId ?: return + val fromDevice = requestInfo.fromDevice ?: return + + 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 + } + + // We don't want to block here + GlobalScope.launch { + if (checkKeysAreDownloaded(senderId, fromDevice) == null) { + Timber.e("## SAS Verification device $fromDevice is not known") + } + } + + // Remember this request + val requestsForUser = pendingRequests[senderId] + ?: ArrayList().also { + pendingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + roomId = event.roomId, + transactionId = event.eventId, + localID = event.eventId!!, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(pendingVerificationRequest) + + /* + * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event + * to begin the verification. + * If both parties send an m.key.verification.start event, and they both specify the same verification method, + * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start + * event is ignored. + * In the case of a single user verifying two of their devices, the device ID is compared instead. + * If both parties send an m.key.verification.start event, but they specify different verification methods, + * the verification should be cancelled with a code of m.unexpected_message. + */ + } + + private suspend fun onRoomStartRequestReceived(event: Event) { + val startReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + val otherUserId = event.senderId + if (startReq?.isValid()?.not() == true) { + Timber.e("## received invalid verification request") + if (startReq.transactionID != null) { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + + handleStart(otherUserId, startReq as VerificationInfoStart) { + it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) + }?.let { + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + it + ) + } + } + + private suspend fun onStartRequestReceived(event: Event) { + Timber.e("## SAS received Start request ${event.eventId}") + val startReq = event.getClearContent().toModel()!! + Timber.v("## SAS received Start request $startReq") + + val otherUserId = event.senderId + if (!startReq.isValid()) { + Timber.e("## SAS received invalid verification request") + if (startReq.transactionID != null) { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + startReq.transactionID, + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + // Download device keys prior to everything + handleStart(otherUserId, startReq) { + it.transport = verificationTransportToDeviceFactory.createTransport(it) + }?.let { + verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + it + ) + } + } + + /** + * 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) { + val tid = startReq.transactionID!! + val existing = getExistingTransaction(otherUserId, tid) + + 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 = DefaultIncomingSASDefaultVerificationTransaction( +// this, + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + startReq.transactionID!!, + otherUserId, + autoAccept).also { txConfigure(it) } + addTransaction(tx) + tx.acceptVerificationEvent(otherUserId, startReq) + 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 + } + } + } else { + return CancelCode.UnexpectedMessage + } + } + + // TODO Refacto: It could just return a boolean + private suspend fun checkKeysAreDownloaded(otherUserId: String, + otherDeviceId: String): MXUsersDevicesMap? { + return try { + var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) + 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(otherDeviceId) == true } + } + } catch (e: Exception) { + null + } + } + + private fun onRoomCancelReceived(event: Event) { + val cancelReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (cancelReq == null || cancelReq.isValid().not()) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let { + updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code))) + // Should we remove it from the list? + } + handleOnCancel(event.senderId!!, cancelReq) + } + + private fun onCancelReceived(event: Event) { + Timber.v("## SAS onCancelReceived") + val cancelReq = event.getClearContent().toModel()!! + + if (!cancelReq.isValid()) { + // ignore + Timber.e("## SAS Received invalid cancel request") + return + } + val otherUserId = event.senderId!! + + handleOnCancel(otherUserId, cancelReq) + } + + private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) { + Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") + + val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!) + val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!) + + if (existingRequest != null) { + // Mark this request as cancelled + updatePendingRequest(existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + )) + } + + if (existingTransaction is SASDefaultVerificationTransaction) { + existingTransaction.state = VerificationTxState.Cancelled(safeValueOf(cancelReq.code), false) + } + } + + private fun onRoomAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept via DM $event") + val accept = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?: return + handleAccept(accept, event.senderId!!) + } + + private fun onAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept $event") + val acceptReq = event.getClearContent().toModel() ?: return + handleAccept(acceptReq, event.senderId!!) + } + + private fun handleAccept(acceptReq: VerificationInfoAccept, senderId: String) { + if (!acceptReq.isValid()) { + // ignore + Timber.e("## SAS Received invalid accept request") + return + } + val otherUserId = senderId + val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) + if (existing == null) { + Timber.e("## SAS Received invalid accept request") + return + } + + if (existing is SASDefaultVerificationTransaction) { + existing.acceptVerificationEvent(otherUserId, acceptReq) + } else { + // not other types now + } + } + + private fun onRoomKeyRequestReceived(event: Event) { + val keyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (keyReq == null || keyReq.isValid().not()) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + handleKeyReceived(event, keyReq) + } + + private fun onKeyReceived(event: Event) { + val keyReq = event.getClearContent().toModel()!! + + if (!keyReq.isValid()) { + // ignore + Timber.e("## SAS Received invalid key request") + return + } + handleKeyReceived(event, keyReq) + } + + private fun handleKeyReceived(event: Event, keyReq: VerificationInfoKey) { + Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") + val otherUserId = event.senderId!! + val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) + if (existing == null) { + Timber.e("## SAS Received invalid key request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.acceptVerificationEvent(otherUserId, keyReq) + } else { + // not other types now + } + } + + private fun onRoomMacReceived(event: Event) { + val macReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (macReq == null || macReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + // TODO should we cancel? + return + } + handleMacReceived(event.senderId, macReq) + } + + private suspend fun onRoomReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + } + } + + private suspend fun onReadyReceived(event: Event) { + val readyReq = event.getClearContent().toModel() + + if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid ready request") + // TODO should we cancel? + return + } + if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { + Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") + // TODO cancel? + return + } + + handleReadyReceived(event.senderId, readyReq) { + verificationTransportToDeviceFactory.createTransport(it) + } + } + + private fun onRoomDoneReceived(event: Event) { + val doneReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + if (doneReq == null || doneReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid Done request") + // TODO should we cancel? + return + } + + handleDoneReceived(event.senderId, doneReq) + } + + private fun onMacReceived(event: Event) { + val macReq = event.getClearContent().toModel()!! + + if (!macReq.isValid() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + return + } + handleMacReceived(event.senderId, macReq) + } + + private fun handleMacReceived(senderId: String, macReq: VerificationInfoMac) { + Timber.v("## SAS Received $macReq") + val existing = getExistingTransaction(senderId, macReq.transactionID!!) + if (existing == null) { + Timber.e("## SAS Received invalid Mac request") + return + } + if (existing is SASDefaultVerificationTransaction) { + existing.acceptVerificationEvent(senderId, macReq) + } else { + // not other types known for now + } + } + + private fun handleReadyReceived(senderId: String, + readyReq: VerificationInfoReady, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) { + 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 + } + + 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, readyReq.fromDevice) + } + + if (readyReq.methods.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 = transportCreator.invoke(tx) + + addTransaction(tx) + } + + updatePendingRequest(existingRequest.copy( + readyInfo = readyReq + )) + } + + private fun createQrCodeData(requestId: String?, otherUserId: String, otherDeviceId: String?): QrCodeData? { + requestId ?: run { + Timber.w("## Unknown requestId") + return null + } + + return when { + userId != otherUserId -> + createQrCodeDataForDistinctUser(requestId, otherUserId, otherDeviceId) + crossSigningService.isCrossSigningVerified() -> + // This is a self verification and I am the old device (Osborne2) + createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) + else -> + // This is a self verification and I am the new device (Dynabook) + createQrCodeDataForUnVerifiedDevice(requestId, otherDeviceId) + } + } + + private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? { + 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() + ?: run { + Timber.w("## Unable to get my fingerprint") + return null + } + + val otherDeviceKey = otherDeviceId + ?.let { + cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() + } + + return QrCodeData( + userId = userId, + requestId = requestId, + action = QrCodeData.ACTION_VERIFY, + keys = hashMapOf( + myMasterKey to myMasterKey, + myDeviceId to myDeviceKey + ), + sharedSecret = generateSharedSecret(), + otherUserKey = otherUserMasterKey, + otherDeviceKey = otherDeviceKey + ) + } + + // Create a QR code to display on the old device (Osborne2) + private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val otherDeviceKey = otherDeviceId + ?.let { + cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() + } + ?: run { + Timber.w("## Unable to get other device data") + return null + } + + val myDeviceId = deviceId + ?: run { + Timber.w("## Unable to get my deviceId") + return null + } + + val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() + ?: run { + Timber.w("## Unable to get my fingerprint") + return null + } + + return QrCodeData( + userId = userId, + requestId = requestId, + action = QrCodeData.ACTION_VERIFY, + keys = hashMapOf( + myMasterKey to myMasterKey, + myDeviceId to myDeviceKey + ), + sharedSecret = generateSharedSecret(), + otherUserKey = null, + otherDeviceKey = otherDeviceKey + ) + } + + // Create a QR code to display on the new device (Dynabook) + private fun createQrCodeDataForUnVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? { + val myMasterKey = crossSigningService.getMyCrossSigningKeys() + ?.masterKey() + ?.unpaddedBase64PublicKey + ?: run { + Timber.w("## Unable to get my master key") + return null + } + + val myDeviceId = deviceId + ?: run { + Timber.w("## Unable to get my deviceId") + return null + } + + val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() + ?: run { + Timber.w("## Unable to get my fingerprint") + return null + } + + val otherDeviceKey = otherDeviceId + ?.let { + cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint() + } + + return QrCodeData( + userId = userId, + requestId = requestId, + action = QrCodeData.ACTION_VERIFY, + keys = hashMapOf( + // Note: no master key here + myDeviceId to myDeviceKey + ), + sharedSecret = generateSharedSecret(), + otherUserKey = myMasterKey, + otherDeviceKey = otherDeviceKey + ) + } + + private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID } + if (existingRequest == null) { + Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionID}") + return + } + updatePendingRequest(existingRequest.copy(isSuccessful = true)) + } + + // TODO All this methods should be delegated to a TransactionStore + override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? { + synchronized(lock = txMap) { + return txMap[otherUserId]?.get(tid) + } + } + + override fun getExistingVerificationRequest(otherUserId: String): List? { + synchronized(lock = pendingRequests) { + return pendingRequests[otherUserId] + } + } + + override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } } + } + } + + override fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest? { + synchronized(lock = pendingRequests) { + return tid?.let { tid -> + pendingRequests.flatMap { entry -> + entry.value.filter { it.roomId == roomId && it.transactionId == tid } + }.firstOrNull() + } + } + } + + private fun getExistingTransactionsForUser(otherUser: String): Collection? { + synchronized(txMap) { + return txMap[otherUser]?.values + } + } + + private fun removeTransaction(otherUser: String, tid: String) { + synchronized(txMap) { + txMap[otherUser]?.remove(tid)?.removeListener(this) + } + } + + private fun addTransaction(tx: DefaultVerificationTransaction) { + tx.otherUserId.let { otherUserId -> + synchronized(txMap) { + val txInnerMap = txMap.getOrPut(otherUserId) { HashMap() } + txInnerMap[tx.transactionId] = tx + dispatchTxAdded(tx) + tx.addListener(this) + } + } + } + + override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? { + val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId) + // should check if already one (and cancel it) + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + txID, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportToDeviceFactory.createTransport(tx) + addTransaction(tx) + + tx.start() + return txID + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun requestKeyVerificationInDMs(methods: List, otherUserId: String, roomId: String, localId: String?) + : PendingVerificationRequest { + Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") + + val requestsForUser = pendingRequests[otherUserId] + ?: ArrayList().also { + pendingRequests[otherUserId] = it + } + + val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) + + // Cancel existing pending requests? + 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 = localId ?: LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = roomId, + localID = localID, + otherUserId = otherUserId + ) + + // We can SCAN or SHOW QR codes only if cross-signing is verified + val methodValues = if (crossSigningService.isCrossSigningVerified()) { + // 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, null) { syncedId, info -> + // We need to update with the syncedID + updatePendingRequest(verificationRequest.copy( + transactionId = syncedId, + // localId stays different + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun requestKeyVerification(methods: List, otherUserId: String, otherDevices: List?): PendingVerificationRequest { + // TODO refactor this with the DM one + Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") + + val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } + val requestsForUser = pendingRequests[otherUserId] + ?: ArrayList().also { + pendingRequests[otherUserId] = it + } + + val transport = verificationTransportToDeviceFactory.createTransport(null) + + // Cancel existing pending requests? + 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)) + existingRequest.targetDevices?.forEach { + transport.cancelTransaction(tid, existingRequest.otherUserId, it, CancelCode.User) + } + } + } + } + + val localID = LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + transactionId = localID, + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + roomId = null, + localID = localID, + otherUserId = otherUserId, + targetDevices = targetDevices + ) + + // We can SCAN or SHOW QR codes only if cross-signing is enabled + val methodValues = if (crossSigningService.isCrossSigningVerified()) { + // 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, null, targetDevices) { _, _ -> + // Nothing special to do in to device mode + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) { + verificationTransportRoomMessageFactory.createTransport(roomId, null) + .cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User) + + getExistingVerificationRequest(otherUserId, transactionId)?.let { + updatePendingRequest(it.copy( + cancelConclusion = CancelCode.User + )) + } + } + + private fun updatePendingRequest(updated: PendingVerificationRequest) { + val requestsForUser = pendingRequests[updated.otherUserId] + ?: ArrayList().also { + pendingRequests[updated.otherUserId] = it + } + val index = requestsForUser.indexOfFirst { + it.transactionId == updated.transactionId + || it.transactionId == null && it.localID == updated.localID + } + if (index != -1) { + requestsForUser.removeAt(index) + } + requestsForUser.add(updated) + dispatchRequestUpdated(updated) + } + + override fun beginKeyVerificationInDMs(method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback?): String? { + if (method == VerificationMethod.SAS) { + val tx = DefaultOutgoingSASDefaultVerificationTransaction( + setDeviceVerificationAction, + userId, + deviceId, + cryptoStore, + crossSigningService, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId) + tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx) + addTransaction(tx) + + tx.start() + return transactionId + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + 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 = verificationTransportRoomMessageFactory.createTransport(roomId, null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportRoomMessageFactory.createTransport(roomId, it) + } + 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, deviceId ?: "", computedMethods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, + readyMsg, + VerificationTxState.None, + CancelCode.User, + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg)) + return true + } else { + Timber.e("## SAS readyPendingVerificationInDMs Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + override fun readyPendingVerification(methods: List, + otherUserId: String, + transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerification $otherUserId 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 = verificationTransportToDeviceFactory.createTransport(null) + val computedMethods = computeReadyMethods( + transactionId, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + existingRequest.requestInfo?.methods, + methods) { + verificationTransportToDeviceFactory.createTransport(it) + } + 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, deviceId ?: "", computedMethods) + transport.sendVerificationReady( + readyMsg, + otherUserId, + existingRequest.requestInfo?.fromDevice ?: "", + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg)) + return true + } else { + Timber.e("## SAS readyPendingVerification Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + + private fun computeReadyMethods( + transactionId: String, + otherUserId: String, + otherDeviceId: String, + otherUserMethods: List?, + methods: List, + transportCreator: (DefaultVerificationTransaction) -> VerificationTransport): 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, otherDeviceId) + + 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 = transportCreator.invoke(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(otherUserId: String, otherDeviceID: String): String { + return buildString { + append(userId).append("|") + append(deviceId).append("|") + append(otherUserId).append("|") + append(otherDeviceID).append("|") + append(UUID.randomUUID().toString()) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + dispatchTxUpdated(tx) + if (tx.state is VerificationTxState.TerminalTxState) { + // 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 71% 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 be3f4c7885..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,18 +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.internal.crypto.model.rest.SendToDeviceObject +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,7 +42,5 @@ internal abstract class VerificationTransaction( listeners.remove(listener) } - abstract fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) - - abstract fun cancel(code: CancelCode) + abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt deleted file mode 100644 index cade637cce..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt +++ /dev/null @@ -1,214 +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.matrix.android.internal.crypto.verification - -import im.vector.matrix.android.api.auth.data.Credentials -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.events.model.EventType -import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept -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.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.JsonCanonicalizer -import timber.log.Timber - -internal class OutgoingSASVerificationRequest( - private val sasVerificationService: DefaultSasVerificationService, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, - deviceFingerprint: String, - transactionId: String, - otherUserId: String, - otherDeviceId: String) - : SASVerificationTransaction( - sasVerificationService, - setDeviceVerificationAction, - credentials, - cryptoStore, - sendToDeviceTask, - taskExecutor, - deviceFingerprint, - transactionId, - otherUserId, - otherDeviceId, - isIncoming = false), - OutgoingSasVerificationRequest { - - override val uxState: OutgoingSasVerificationRequest.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 - } - } - - override fun onVerificationStart(startReq: KeyVerificationStart) { - Timber.e("## onVerificationStart - unexpected id:$transactionId") - cancel(CancelCode.UnexpectedMessage) - } - - fun start() { - if (state != SasVerificationTxState.None) { - Timber.e("## start verification from invalid state") - // should I cancel?? - throw IllegalStateException("Interactive Key verification already started") - } - - val startMessage = KeyVerificationStart() - startMessage.fromDevice = credentials.deviceId - startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS - startMessage.transactionID = transactionId - startMessage.keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS - startMessage.hashes = KNOWN_HASHES - startMessage.messageAuthenticationCodes = KNOWN_MACS - startMessage.shortAuthenticationStrings = KNOWN_SHORT_CODES - - startReq = startMessage - state = SasVerificationTxState.SendingStart - - sendToOther( - EventType.KEY_VERIFICATION_START, - startMessage, - SasVerificationTxState.Started, - CancelCode.User, - null - ) - } - - override fun onVerificationAccept(accept: KeyVerificationAccept) { - Timber.v("## onVerificationAccept id:$transactionId") - if (state != SasVerificationTxState.Started) { - Timber.e("## received accept request from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - // Check that the agreement is correct - if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) - || !KNOWN_HASHES.contains(accept.hash) - || !KNOWN_MACS.contains(accept.messageAuthenticationCode) - || accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { - Timber.e("## received accept request from invalid state") - cancel(CancelCode.UnknownMethod) - return - } - - // 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 - - // 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 - val pubKey = getSAS().publicKey - - val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) - // we need to send this to other device now - state = SasVerificationTxState.SendingKey - sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.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 - } - } - } - - override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { - Timber.v("## onKeyVerificationKey id:$transactionId") - if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { - Timber.e("## received key from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - otherKey = vKey.key - // Upon receipt of the m.key.verification.key message from Bob’s device, - // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept - // message is the same as the expected value based on the value of the key property received - // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. - - // check commitment - val concat = vKey.key + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) - val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" - - if (accepted!!.commitment.equals(otherCommitment)) { - getSAS().setTheirPublicKey(otherKey) - // (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, - // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: - // - the string “MATRIX_KEY_VERIFICATION_SAS”, - // - the Matrix ID of the user who sent the m.key.verification.start message, - // - the device ID of the device that sent the m.key.verification.start message, - // - 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 - // decimal: generate five bytes by using HKDF. - // emoji: generate six bytes by using HKDF. - shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) - state = SasVerificationTxState.ShortCodeReady - } else { - // bad commitement - cancel(CancelCode.MismatchedCommitment) - } - } - - override fun onKeyVerificationMac(vKey: KeyVerificationMac) { - Timber.v("## onKeyVerificationMac id:$transactionId") - if (state != SasVerificationTxState.OnKeyReceived - && state != SasVerificationTxState.ShortCodeReady - && state != SasVerificationTxState.ShortCodeAccepted - && state != SasVerificationTxState.SendingMac - && state != SasVerificationTxState.MacSent) { - Timber.e("## received key from invalid state $state") - cancel(CancelCode.UnexpectedMessage) - return - } - - theirMac = vKey - - // Do I have my Mac? - if (myMac != null) { - // I can check - verifyMacs() - } - // Wait for ShortCode Accepted - } -} 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 new file mode 100644 index 0000000000..3379ddd2ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,56 @@ +/* + * 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.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.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 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, + val isIncoming: Boolean = false, + val localID: String = UUID.randomUUID().toString(), + val otherUserId: String, + val roomId: String?, + val transactionId: String? = null, + val requestInfo: VerificationInfoRequest? = null, + val readyInfo: VerificationInfoReady? = null, + val cancelConclusion: CancelCode? = null, + val isSuccessful: Boolean = false, + val handledByOtherSession: Boolean = false, + // In case of to device it is sent to a list of devices + val targetDevices: List? = null +) { + val isReady: Boolean = readyInfo != null + val isSent: Boolean = transactionId != null + + val isFinished: Boolean = isSuccessful || cancelConclusion != null + + fun hasMethod(method: VerificationMethod): Boolean? { + return when (method) { + 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 62% 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 589103d38a..22f543f267 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 @@ -17,43 +17,38 @@ package im.vector.matrix.android.internal.crypto.verification import android.os.Build import im.vector.matrix.android.api.MatrixCallback -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.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.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.extensions.toUnsignedInt -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.withoutPrefix import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber -import kotlin.properties.Delegates /** * Represents an ongoing short code interactive key verification between two devices. */ -internal abstract class SASVerificationTransaction( - private val sasVerificationService: DefaultSasVerificationService, +internal abstract class SASDefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, + open val userId: String, + open val deviceId: String?, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, + private val crossSigningService: CrossSigningService, private val deviceFingerprint: String, transactionId: String, otherUserId: String, - otherDevice: String?, - isIncoming: Boolean) : - VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) { + otherDeviceId: String?, + isIncoming: Boolean +) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction { companion object { const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" @@ -75,33 +70,32 @@ internal abstract class SASVerificationTransaction( } } - override var state by Delegates.observable(SasVerificationTxState.None) { _, _, new -> - // println("$property has changed from $old to $new") - listeners.forEach { - try { - it.transactionUpdated(this) - } catch (e: Throwable) { - Timber.e(e, "## Error while notifying listeners") + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + + if (newState is VerificationTxState.TerminalTxState) { + releaseSAS() } } - if (new == SasVerificationTxState.Cancelled - || new == SasVerificationTxState.OnCancelled - || new == SasVerificationTxState.Verified) { - releaseSAS() - } - } - - override var cancelledReason: CancelCode? = null private var olmSas: OlmSAS? = null - var startReq: KeyVerificationStart? = null - var accepted: KeyVerificationAccept? = null + var startReq: VerificationInfoStart? = null + var accepted: VerificationInfoAccept? = null var otherKey: String? = null var shortCodeBytes: ByteArray? = null - var myMac: KeyVerificationMac? = null - var theirMac: KeyVerificationMac? = null + var myMac: VerificationInfoMac? = null + var theirMac: VerificationInfoMac? = null fun getSAS(): OlmSAS { if (olmSas == null) olmSas = OlmSAS() @@ -125,14 +119,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: @@ -143,15 +137,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 @@ -160,13 +176,13 @@ internal abstract class SASVerificationTransaction( return } - val macMsg = KeyVerificationMac.create(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 } } @@ -176,29 +192,38 @@ internal abstract class SASVerificationTransaction( } // if not wait for it } - override fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) { - when (event) { - is KeyVerificationStart -> onVerificationStart(event) - is KeyVerificationAccept -> onVerificationAccept(event) - is KeyVerificationKey -> onKeyVerificationKey(senderId, event) - is KeyVerificationMac -> onKeyVerificationMac(event) - else -> { + override fun shortCodeDoesNotMatch() { + Timber.v("## SAS short code do not match for id:$transactionId") + 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(info) + is VerificationInfoMac -> onKeyVerificationMac(info) + else -> { // nop } } } - abstract fun onVerificationStart(startReq: KeyVerificationStart) + abstract fun onVerificationStart(startReq: VerificationInfoStart) - abstract fun onVerificationAccept(accept: KeyVerificationAccept) + abstract fun onVerificationAccept(accept: VerificationInfoAccept) - abstract fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) + abstract fun onKeyVerificationKey(vKey: VerificationInfoKey) - abstract fun onKeyVerificationMac(vKey: KeyVerificationMac) + 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) @@ -210,7 +235,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(",") @@ -226,41 +251,87 @@ 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("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 } val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) if (mac != theirMac?.mac?.get(it)) { // WRONG! + Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") cancel(CancelCode.MismatchedKeys) return } 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()) { - Timber.e("Verification: No devices verified") + 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) } - state = SasVerificationTxState.Verified + transport.done(transactionId) + 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() { @@ -268,43 +339,16 @@ internal abstract class SASVerificationTransaction( } override fun cancel(code: CancelCode) { - cancelledReason = code - state = SasVerificationTxState.Cancelled - sasVerificationService.cancelTransaction( - transactionId, - otherUserId, - otherDeviceId ?: "", - code) + state = VerificationTxState.Cancelled(code, true) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) } protected fun sendToOther(type: String, - keyToDevice: Any, - nextState: SasVerificationTxState, + keyToDevice: VerificationInfo, + nextState: VerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { - val contentMap = MXUsersDevicesMap() - contentMap.setObject(otherUserId, otherDeviceId, keyToDevice) - - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.") - if (onDone != null) { - onDone() - } else { - state = nextState - } - } - - override fun onFailure(failure: Throwable) { - Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") - - cancel(onErrorReason) - } - } - } - .executeBy(taskExecutor) + transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) } fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt new file mode 100644 index 0000000000..2b049e0061 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -0,0 +1,79 @@ +/* + * 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 + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.shouldBeRetried +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask +import im.vector.matrix.android.internal.worker.SessionWorkerParams +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +internal class SendVerificationMessageWorker(context: Context, + params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + val event: Event, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + @Inject + lateinit var sendVerificationMessageTask: SendVerificationMessageTask + + @Inject + lateinit var cryptoService: CryptoService + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.sessionId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") + } + sessionComponent.inject(this) + val localId = params.event.eventId ?: "" + return try { + val eventId = sendVerificationMessageTask.execute( + SendVerificationMessageTask.Params( + event = params.event, + cryptoService = cryptoService + ) + ) + + Result.success(Data.Builder().putString(localId, eventId).build()) + } catch (exception: Throwable) { + if (exception.shouldBeRetried()) { + Result.retry() + } else { + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt new file mode 100644 index 0000000000..ed2e5dfbe0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt @@ -0,0 +1,35 @@ +/* + * 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.verification + +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject + +interface VerificationInfo { + fun toEventContent(): Content? = null + fun toSendToDeviceObject(): SendToDeviceObject? = null + fun isValid(): Boolean + + /** + * String to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + val transactionID: String? + + // TODO Refacto Put the relatesTo here or at least in Message sent in Room parent? + // val relatesTo: RelationDefaultContent? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt new file mode 100644 index 0000000000..cc6c8c8530 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt @@ -0,0 +1,54 @@ +/* + * 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.verification + +internal interface VerificationInfoAccept : VerificationInfo { + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val keyAgreementProtocol: String? + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val hash: String? + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val messageAuthenticationCode: String? + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + val shortAuthenticationStrings: List? + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + var commitment: String? +} + +internal interface VerificationInfoAcceptFactory { + + fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt new file mode 100644 index 0000000000..713453e93e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt @@ -0,0 +1,28 @@ +/* + * 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.verification + +internal interface VerificationInfoCancel : VerificationInfo { + /** + * machine-readable reason for cancelling, see [CancelCode] + */ + val code: String? + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + val reason: String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt new file mode 100644 index 0000000000..9c7c00e84b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt @@ -0,0 +1,18 @@ +/* + * 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 + +interface VerificationInfoDone : VerificationInfo diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt new file mode 100644 index 0000000000..2466660ca4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt @@ -0,0 +1,30 @@ +/* + * 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.verification + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +internal interface VerificationInfoKey : VerificationInfo { + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + val key: String? +} + +internal interface VerificationInfoKeyFactory { + fun create(tid: String, pubKey: String): VerificationInfoKey +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt new file mode 100644 index 0000000000..e895973ae8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt @@ -0,0 +1,35 @@ +/* + * 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.verification + +internal interface VerificationInfoMac : VerificationInfo { + /** + * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key + */ + val mac: Map? + + /** + * The MAC of the comma-separated, sorted list of key IDs given in the mac property, + * as an unpadded base64 string, calculated using the MAC key. + * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will + * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. + */ + val keys: String? +} + +internal interface VerificationInfoMacFactory { + fun create(tid: String, mac: Map, keys: String) : VerificationInfoMac +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000..4c26951315 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,40 @@ +/* + * 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.verification + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ + +interface VerificationInfoReady : VerificationInfo { + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List? +} + +internal interface MessageVerificationReadyFactory { + fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoRequest.kt new file mode 100644 index 0000000000..529334c3ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoRequest.kt @@ -0,0 +1,36 @@ +/* + * 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.verification + +interface VerificationInfoRequest : VerificationInfo { + + /** + * Required. The device ID which is initiating the request. + */ + val fromDevice: String? + + /** + * Required. The verification methods supported by the sender. + */ + val methods: List? + + /** + * The POSIX timestamp in milliseconds for when the request was made. + * 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. + */ + val timestamp: Long? +} 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 new file mode 100644 index 0000000000..3f3e4f27a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -0,0 +1,61 @@ +/* + * 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.verification + +internal interface VerificationInfoStart : VerificationInfo { + + val method: String? + + /** + * Alice’s device ID + */ + val fromDevice: String? + + /** + * An array of key agreement protocols that Alice’s client understands. + * Must include “curve25519”. + * Other methods may be defined in the future + */ + val keyAgreementProtocols: List? + + /** + * An array of hashes that Alice’s client understands. + * Must include “sha256”. Other methods may be defined in the future. + */ + val hashes: List? + + /** + * An array of message authentication codes that Alice’s client understands. + * Must include “hkdf-hmac-sha256”. + * Other methods may be defined in the future. + */ + val messageAuthenticationCodes: List? + + /** + * An array of short authentication string methods that Alice’s client (and Alice) understands. + * Must include “decimal”. + * This document also describes the “emoji” method. + * Other methods may be defined in the future + */ + 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 new file mode 100644 index 0000000000..4eab1748b8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -0,0 +1,73 @@ +/* + * 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.verification + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.crypto.CryptoService +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.internal.crypto.tasks.DefaultRoomVerificationUpdateTask +import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.whereTypes +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import io.realm.OrderedCollectionChangeSet +import io.realm.RealmConfiguration +import io.realm.RealmResults +import javax.inject.Inject + +internal class VerificationMessageLiveObserver @Inject constructor( + @SessionDatabase realmConfiguration: RealmConfiguration, + private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, + private val cryptoService: CryptoService, + private val verificationService: DefaultVerificationService, + private val taskExecutor: TaskExecutor +) : RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.whereTypes(it, listOf( + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, + EventType.MESSAGE, + EventType.ENCRYPTED) + ) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + // Should we ignore when it's an initial sync? + val events = changeSet.insertions + .asSequence() + .mapNotNull { results[it]?.asDomain() } + .filterNot { + // ignore local echos + LocalEcho.isLocalEchoId(it.eventId ?: "") + } + .toList() + + roomVerificationUpdateTask.configureWith( + RoomVerificationUpdateTask.Params(events, verificationService, cryptoService) + ).executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt new file mode 100644 index 0000000000..bc85cddf26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -0,0 +1,89 @@ +/* + * 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.verification + +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState + +/** + * Verification can be performed using toDevice events or via DM. + * This class abstracts the concept of transport for verification + */ +internal interface VerificationTransport { + + /** + * Sends a message + */ + fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) + + fun sendVerificationRequest(supportedMethods: List, + localID: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, VerificationInfoRequest?) -> Unit) + + fun cancelTransaction(transactionId: String, + otherUserId: String, + otherUserDeviceId: String, + code: CancelCode) + + fun done(transactionId: String) + + /** + * Creates an accept message suitable for this transport + */ + fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept + + fun createKey(tid: String, + pubKey: String): VerificationInfoKey + + /** + * 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 + + fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady + + // TODO Refactor + fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String, + callback: (() -> Unit)?) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt new file mode 100644 index 0000000000..30d94c52d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -0,0 +1,362 @@ +/* + * 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.verification + +import androidx.lifecycle.Observer +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.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 +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.util.StringProvider +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +internal class VerificationTransportRoomMessage( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + private val sessionId: String, + private val userId: String, + private val userDeviceId: String?, + private val roomId: String, + private val monarchy: Monarchy, + private val localEchoEventFactory: LocalEchoEventFactory, + private val tx: DefaultVerificationTransaction? +) : VerificationTransport { + + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = verificationInfo.toEventContent()!! + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + +// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { +// override fun onSuccess(result: Operation.State.SUCCESS?) { +// if (onDone != null) { +// onDone() +// } else { +// tx?.state = nextState +// } +// } +// +// override fun onFailure(t: Throwable) { +// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") +// tx?.cancel(onErrorReason) +// } +// }, listenerExecutor) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { wInfo -> + if (wInfo.outputData.getBoolean("failed", false)) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } else { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun sendVerificationRequest(supportedMethods: List, + localID: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, VerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + // This transport requires a room + requireNotNull(roomId) + + val info = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = userDeviceId ?: "", + toUserId = otherUserId, + timestamp = System.currentTimeMillis(), + methods = supportedMethods + ) + val content = info.toContent() + + val event = createEventAndLocalEcho( + localID, + EventType.MESSAGE, + roomId, + content + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + + workManagerProvider.workManager + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == workRequest.id } + ?.let { wInfo -> + if (wInfo.outputData.getBoolean("failed", false)) { + callback(null, null) + } else if (wInfo.outputData.getString(localID) != null) { + callback(wInfo.outputData.getString(localID), info) + } else { + callback(null, null) + } + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_CANCEL, + roomId = roomId, + content = MessageVerificationCancelContent.create(transactionId, code).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + enqueueSendWork(workerParams) + } + + override fun done(transactionId: String) { + Timber.d("## SAS sending done for $transactionId") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_DONE, + roomId = roomId, + content = MessageVerificationDoneContent( + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + sessionId = sessionId, + event = event + )) + enqueueSendWork(workerParams) + } + + private fun enqueueSendWork(workerParams: Data): Pair { + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + return workManagerProvider.workManager + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() to workRequest.id + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List) + : VerificationInfoAccept = MessageVerificationAcceptContent.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings + ) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + hashes, + keyAgreementProtocols, + messageAuthenticationCodes, + shortAuthenticationStrings, + 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 + ) + } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = tid + ), + methods = methods + ) + } + + private fun createEventAndLocalEcho(localID: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localID, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localID) + ).also { + localEchoEventFactory.createLocalEcho(it) + } + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String, + callback: (() -> Unit)?) { + // Not applicable (send event is called directly) + Timber.w("## SAS ignored verification ready with methods: ${keyReq.methods}") + } +} + +internal class VerificationTransportRoomMessageFactory @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + private val stringProvider: StringProvider, + private val monarchy: Monarchy, + @SessionId + private val sessionId: String, + @UserId + private val userId: String, + @DeviceId + private val deviceId: String?, + private val localEchoEventFactory: LocalEchoEventFactory) { + + 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/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt new file mode 100644 index 0000000000..f76f8331bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -0,0 +1,247 @@ +/* + * 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.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.VerificationTxState +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.message.MessageType +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.KeyVerificationDone +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.KeyVerificationRequest +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.di.DeviceId +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 VerificationTransportToDevice( + private var tx: DefaultVerificationTransaction?, + private var sendToDeviceTask: SendToDeviceTask, + private val myDeviceId: String?, + private var taskExecutor: TaskExecutor +) : VerificationTransport { + + override fun sendVerificationRequest(supportedMethods: List, + localID: String, + otherUserId: String, + roomId: String?, + toDevices: List?, + callback: (String?, VerificationInfoRequest?) -> Unit) { + Timber.d("## SAS sending verification request with supported methods: $supportedMethods") + val contentMap = MXUsersDevicesMap() + val keyReq = KeyVerificationRequest( + fromDevice = myDeviceId, + methods = supportedMethods, + timestamp = System.currentTimeMillis(), + transactionID = localID + ) + toDevices?.forEach { + contentMap.setObject(otherUserId, it, keyReq) + } + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localID)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback.invoke(localID, keyReq) + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun sendVerificationReady(keyReq: VerificationInfoReady, + otherUserId: String, + otherDeviceId: String, + callback: (() -> Unit)?) { + Timber.d("## SAS sending verification ready with methods: ${keyReq.methods}") + val contentMap = MXUsersDevicesMap() + + contentMap.setObject(otherUserId, otherDeviceId, keyReq) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_READY, contentMap)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## verification [$tx.transactionId] send toDevice request success") + callback?.invoke() + } + + override fun onFailure(failure: Throwable) { + Timber.e("## verification [$tx.transactionId] failed to send toDevice request") + } + } + } + .executeBy(taskExecutor) + } + + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: VerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val tx = tx ?: return + val contentMap = MXUsersDevicesMap() + val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() + ?: return Unit.also { tx.cancel() } + + contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.") + if (onDone != null) { + onDone() + } else { + tx.state = nextState + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") + tx.cancel(onErrorReason) + } + } + } + .executeBy(taskExecutor) + } + + override fun done(transactionId: String) { + val otherUserId = tx?.otherUserId ?: return + val otherUserDeviceId = tx?.otherDeviceId ?: return + val cancelMessage = KeyVerificationDone(transactionId) + val contentMap = MXUsersDevicesMap() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] done") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to done.") + } + } + } + .executeBy(taskExecutor) + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap() + contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage) + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + } + } + .executeBy(taskExecutor) + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerificationInfoAccept = KeyVerificationAccept.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings) + + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) + + override fun createStartForSas(fromDevice: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return KeyVerificationStart( + fromDevice, + VERIFICATION_METHOD_SAS, + transactionID, + keyAgreementProtocols, + hashes, + messageAuthenticationCodes, + 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 { + return KeyVerificationReady( + transactionID = tid, + fromDevice = fromDevice, + methods = methods + ) + } +} + +internal class VerificationTransportToDeviceFactory @Inject constructor( + private val sendToDeviceTask: SendToDeviceTask, + @DeviceId val myDeviceId: String?, + private val taskExecutor: TaskExecutor) { + + fun createTransport(tx: DefaultVerificationTransaction?): VerificationTransportToDevice { + return VerificationTransportToDevice(tx, sendToDeviceTask, myDeviceId, 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..d1b72f54c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -0,0 +1,279 @@ +/* + * 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 + +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 val qrCodeText: String? + get() = qrCodeData?.toUrl() + + override var state: VerificationTxState = VerificationTxState.None + set(newState) { + field = newState + + 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.requestId != transactionId) { + Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestId} expected:$transactionId") + cancel(CancelCode.QrCodeInvalid) + return + } + + // check master key + if (otherQrCodeData.userId != userId + && otherQrCodeData.otherUserKey == null) { + // Verification with other user, other_user_key is mandatory in this case + Timber.d("## Verification QR: Invalid, missing other_user_key") + cancel(CancelCode.QrCodeInvalid) + return + } + + if (otherQrCodeData.otherUserKey != null + && otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { + Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}") + cancel(CancelCode.MismatchedKeys) + return + } + + // Check device key if available + if (otherQrCodeData.otherDeviceKey != null + && otherQrCodeData.otherDeviceKey != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { + Timber.d("## Verification QR: Invalid other device key") + 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) + + val safeOtherDeviceId = otherDeviceId + if (!otherQrCodeData.otherDeviceKey.isNullOrBlank() + && safeOtherDeviceId != null) { + // Locally verify the device + toVerifyDeviceIds.add(safeOtherDeviceId) + } + + // Trust the other user + trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct()) + } + + 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) { + state = VerificationTxState.Cancelled(code, true) + 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 + // But first, ask the user for a confirmation + state = VerificationTxState.QrScannedByOther + } else { + // Display a warning + cancel(CancelCode.MismatchedKeys) + } + } + + override fun otherUserScannedMyQrCode() { + trust(true, emptyList()) + } + + override fun otherUserDidNotScannedMyQrCode() { + // What can I do then? + // At least remove the transaction... + state = VerificationTxState.Cancelled(CancelCode.MismatchedKeys, true) + } + + 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..d539152135 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt @@ -0,0 +1,131 @@ +/* + * 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=
+ *     &other_device_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%2Fi5NqRREDACTEDRPxBIuqK8Usl6Y3big
+ *     &other_device_key=WqSVLkBREDACTEDBsfszdvsdBEvefqsdcsfBvsfcsFb
+ * 
+ */ +fun QrCodeData.toUrl(): String { + return buildString { + append(PermalinkFactory.createPermalink(userId)) + append("?request=") + append(URLEncoder.encode(requestId, ENCODING)) + append("&action=") + append(URLEncoder.encode(action, ENCODING)) + + for ((keyId, key) in keys) { + append("&key_${URLEncoder.encode(keyId, ENCODING)}=") + append(URLEncoder.encode(key, ENCODING)) + } + + append("&secret=") + append(URLEncoder.encode(sharedSecret, ENCODING)) + + if (!otherUserKey.isNullOrBlank()) { + append("&other_user_key=") + append(URLEncoder.encode(otherUserKey, ENCODING)) + } + if (!otherDeviceKey.isNullOrBlank()) { + append("&other_device_key=") + append(URLEncoder.encode(otherDeviceKey, 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"]?.takeIf { it.isNotBlank() } ?: return null + + val requestEventId = keyValues["request"]?.takeIf { it.isNotBlank() } ?: return null + val sharedSecret = keyValues["secret"]?.takeIf { it.isNotBlank() } ?: return null + val otherUserKey = keyValues["other_user_key"] + val otherDeviceKey = keyValues["other_device_key"] + + val keys = keyValues.keys + .filter { it.startsWith("key_") } + .map { + URLDecoder.decode(it.substringAfter("key_"), ENCODING) to (keyValues[it] ?: return null) + } + .toMap() + + return QrCodeData( + userId, + requestEventId, + action, + keys, + sharedSecret, + otherUserKey, + otherDeviceKey + ) +} 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..0f9a31ab32 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.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.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, + // Request Id. Can be an arbitrary value. In DM, it will be the event ID of the associated verification request event. + val requestId: 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. In the case where a device does not have a cross-signing key + // (as in the case where a user logs in to a new device, and is verifying against another device), thin the QR code should contain at + // least the device's 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?, + // The other device's key, in unpadded base64 + // This is only needed when a user is verifying their own devices, where the other device has not yet been signed with the cross-signing key. + val otherDeviceKey: 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/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 701d35926a..ccdb8fb91f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -19,9 +19,11 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary +import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedSummary import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity import io.realm.RealmList internal object EventAnnotationsSummaryMapper { @@ -45,6 +47,14 @@ internal object EventAnnotationsSummaryMapper { it.sourceLocalEchoEvents.toList(), it.lastEditTs ) + }, + referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { + ReferencesAggregatedSummary( + it.eventId, + ContentMapper.map(it.content), + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) } ) } @@ -75,6 +85,14 @@ internal object EventAnnotationsSummaryMapper { }) } } + eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let { + ReferencesAggregatedSummaryEntity( + it.eventId, + ContentMapper.map(it.content), + RealmList().apply { addAll(it.sourceEvents) }, + RealmList().apply { addAll(it.localEchos) } + ) + } return eventAnnotationsSummaryEntity } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 65fd382960..75eb61274d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -71,6 +71,7 @@ internal object EventMapper { unsignedData = ud, redacts = eventEntity.redacts ).also { + it.ageLocalTs = eventEntity.ageLocalTs it.sendState = eventEntity.sendState eventEntity.decryptionResultJson?.let { json -> try { @@ -91,8 +92,9 @@ internal fun EventEntity.asDomain(): Event { return EventMapper.map(this) } -internal fun Event.toEntity(roomId: String, sendState: SendState): EventEntity { +internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity { return EventMapper.map(this, roomId).apply { this.sendState = sendState + this.ageLocalTs = ageLocalTs } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt index 470772a40e..5e8db4cf84 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt @@ -26,7 +26,8 @@ internal object RoomMemberSummaryMapper { userId = roomMemberSummaryEntity.userId, avatarUrl = roomMemberSummaryEntity.avatarUrl, displayName = roomMemberSummaryEntity.displayName, - membership = roomMemberSummaryEntity.membership + membership = roomMemberSummaryEntity.membership, + userEncryptionTrustLevel = null ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt index 523d94b770..1a4f72f0b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -24,7 +24,8 @@ internal open class EventAnnotationsSummaryEntity( var eventId: String = "", var roomId: String? = null, var reactionsSummary: RealmList = RealmList(), - var editSummary: EditAggregatedSummaryEntity? = null + var editSummary: EditAggregatedSummaryEntity? = null, + var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 834cb7f520..df64b20b44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -38,7 +38,8 @@ internal open class EventEntity(@PrimaryKey var eventId: String = "", var unsignedData: String? = null, var redacts: String? = null, var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null + var decryptionErrorCode: String? = null, + var ageLocalTs: Long? = null ) : RealmObject() { private var sendStateStr: String = SendState.UNKNOWN.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..1c3ea70e52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt @@ -0,0 +1,31 @@ +/* + * 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class ReferencesAggregatedSummaryEntity( + var eventId: String = "", + var content: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 48c16bc26a..298e887f0f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -40,6 +40,7 @@ import io.realm.annotations.RealmModule EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, + ReferencesAggregatedSummaryEntity::class, PushRulesEntity::class, PushRuleEntity::class, PushConditionEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index 1e7a5758ad..03025cb9bb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -47,3 +47,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId } return obj } +internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId).apply { this.roomId = roomId } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt new file mode 100644 index 0000000000..7d2ed98232 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -0,0 +1,35 @@ +/* + * 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.database.query + +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReferencesAggregatedSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(ReferencesAggregatedSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, txID: String): ReferencesAggregatedSummaryEntity { + return realm.createObject(ReferencesAggregatedSummaryEntity::class.java).apply { + this.eventId = txID + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 04b8565546..284cbfff88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.olm.OlmManager +import java.util.concurrent.Executors @Module internal object MatrixModule { @@ -35,9 +36,10 @@ internal object MatrixModule { @MatrixScope fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { return MatrixCoroutineDispatchers(io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher() + computation = Dispatchers.Default, + main = Dispatchers.Main, + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), + dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index 793be10880..98cf9e234e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -46,6 +46,7 @@ object MoshiProvider { .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) ) .add(SerializeNulls.JSON_ADAPTER_FACTORY) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 32649443db..a0a36d9982 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -25,6 +25,13 @@ import javax.inject.Qualifier @Retention(AnnotationRetention.RUNTIME) internal annotation class UserId +/** + * Used to inject the deviceId + */ +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class DeviceId + /** * Used to inject the md5 of the userId */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt new file mode 100644 index 0000000000..82091be697 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/WorkManagerProvider.kt @@ -0,0 +1,58 @@ +/* + * 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.di + +import android.content.Context +import androidx.work.* +import javax.inject.Inject + +internal class WorkManagerProvider @Inject constructor( + context: Context, + @SessionId private val sessionId: String +) { + private val tag = MATRIX_SDK_TAG_PREFIX + sessionId + + val workManager = WorkManager.getInstance(context) + + /** + * Create a OneTimeWorkRequestBuilder, with the Matrix SDK tag + */ + inline fun matrixOneTimeWorkRequestBuilder() = + OneTimeWorkRequestBuilder() + .addTag(tag) + + /** + * Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions + */ + fun cancelAllWorks() { + workManager.let { + it.cancelAllWorkByTag(tag) + it.pruneWork() + } + } + + companion object { + private const val MATRIX_SDK_TAG_PREFIX = "MatrixSDK-" + + /** + * Default constraints: connected network + */ + val workConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index f33ec2f88a..2a2076db6c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.network -import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.GlobalError @@ -109,7 +108,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, eventBus: EventBu return Failure.ServerError(matrixError, httpCode) } - } catch (ex: JsonDataException) { + } catch (ex: Exception) { // This is not a MatrixError Timber.w("The error returned by the server is not a MatrixError") } catch (ex: JsonEncodingException) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index e22e47bc1c..537bc63355 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session -import android.content.Context import androidx.annotation.MainThread import androidx.lifecycle.LiveData import dagger.Lazy @@ -46,6 +45,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread @@ -63,7 +63,7 @@ import javax.inject.Provider @SessionScope internal class DefaultSession @Inject constructor( override val sessionParams: SessionParams, - private val context: Context, + private val workManagerProvider: WorkManagerProvider, private val eventBus: EventBus, @SessionId override val sessionId: String, @@ -122,15 +122,15 @@ internal class DefaultSession @Inject constructor( } override fun requireBackgroundSync() { - SyncWorker.requireBackgroundSync(context, sessionId) + SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) } override fun startAutomaticBackgroundSync(repeatDelay: Long) { - SyncWorker.automaticallyBackgroundSync(context, sessionId, 0, repeatDelay) + SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) } override fun stopAnyBackgroundSync() { - SyncWorker.stopAnyBackgroundSync(context) + SyncWorker.stopAnyBackgroundSync(workManagerProvider) } override fun startSync(fromForeground: Boolean) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 5b6aa57979..4a0e4424d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -21,6 +21,7 @@ import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.di.SessionAssistedInjectModule import im.vector.matrix.android.internal.network.NetworkConnectivityChecker @@ -100,6 +101,8 @@ internal interface SessionComponent { fun inject(addHttpPusherWorker: AddHttpPusherWorker) + fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index af04f901cb..bc0038f0c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* @@ -84,6 +85,13 @@ internal abstract class SessionModule { return credentials.userId } + @JvmStatic + @DeviceId + @Provides + fun providesDeviceId(credentials: Credentials): String? { + return credentials.deviceId + } + @JvmStatic @UserMd5 @Provides @@ -220,6 +228,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver + @Binds + @IntoSet + abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver + @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index a60bc78b6c..3ca5a03822 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -16,9 +16,7 @@ package im.vector.matrix.android.internal.session.group -import android.content.Context import androidx.work.ExistingWorkPolicy -import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.RealmLiveEntityObserver @@ -27,8 +25,7 @@ import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionId -import im.vector.matrix.android.internal.worker.WorkManagerUtil -import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.worker.WorkerParamsFactory import io.realm.OrderedCollectionChangeSet import io.realm.RealmResults @@ -38,7 +35,7 @@ import javax.inject.Inject private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" internal class GroupSummaryUpdater @Inject constructor( - private val context: Context, + private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val monarchy: Monarchy) : RealmLiveEntityObserver(monarchy.realmConfiguration) { @@ -72,12 +69,12 @@ internal class GroupSummaryUpdater @Inject constructor( val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) - val sendWork = matrixOneTimeWorkRequestBuilder() + val sendWork = workManagerProvider.matrixOneTimeWorkRequestBuilder() .setInputData(workData) - .setConstraints(WorkManagerUtil.workConstraints) + .setConstraints(WorkManagerProvider.workConstraints) .build() - WorkManager.getInstance(context) + workManagerProvider.workManager .beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, sendWork) .enqueue() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt index cdbf6aeee4..9d80223149 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt @@ -15,10 +15,8 @@ */ package im.vector.matrix.android.internal.session.pushers -import android.content.Context import androidx.lifecycle.LiveData import androidx.work.BackoffPolicy -import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.pushers.Pusher @@ -27,17 +25,16 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.worker.WorkManagerUtil -import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject internal class DefaultPusherService @Inject constructor( - private val context: Context, + private val workManagerProvider: WorkManagerProvider, private val monarchy: Monarchy, @SessionId private val sessionId: String, private val getPusherTask: GetPushersTask, @@ -68,12 +65,12 @@ internal class DefaultPusherService @Inject constructor( val params = AddHttpPusherWorker.Params(sessionId, pusher) - val request = matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) + val request = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) .setInputData(WorkerParamsFactory.toData(params)) .setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS) .build() - WorkManager.getInstance(context).enqueue(request) + workManagerProvider.workManager.enqueue(request) return request.id } 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 cf4c858bbd..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 @@ -74,6 +76,26 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } } + override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val candidates = RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll()?.filter { dm -> + dm.otherMemberIds.contains(otherUserId) + && dm.membership == Membership.JOIN + }?.map { + it.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 + } + } + override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { return monarchy .fetchCopyMap({ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 3d7c5df5fc..def7f2c2f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -18,15 +18,30 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy 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.events.model.* +import im.vector.matrix.android.api.session.events.model.AggregatedAnnotation +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.toContent +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent 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.relation.ReactionContent import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.EventMapper -import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -42,6 +57,18 @@ internal interface EventRelationsAggregationTask : Task { + EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") handleReaction(event, roomId, realm, userId, isLocalEcho) } - EventType.MESSAGE -> { + EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) @@ -99,33 +126,51 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } - EventType.ENCRYPTED -> { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + event.content.toModel()?.relatesTo?.let { + if (it.type == RelationType.REFERENCE && it.eventId != null) { + handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + } + } + } + + EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) { // we need to decrypt if needed - if (event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, event.roomId) - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.w("Failed to decrypt e2e replace") - // TODO -> we should keep track of this and retry, or aggregation will be broken - } - } + decryptIfNeeded(event) event.getClearContent().toModel()?.let { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { + decryptIfNeeded(event) + when (event.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + encryptedEventContent.relatesTo.eventId?.let { + handleVerification(realm, event, roomId, isLocalEcho, it, userId) + } + } + } } } - EventType.REDACTION -> { + EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return@forEach when (eventToPrune.type) { @@ -145,7 +190,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } } - else -> Timber.v("UnHandled event ${event.eventId}") + else -> Timber.v("UnHandled event ${event.eventId}") } } catch (t: Throwable) { Timber.e(t, "## Should not happen ") @@ -153,16 +198,29 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } + private fun decryptIfNeeded(event: Event) { + if (event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId ?: "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.w("Failed to decrypt e2e replace") + // TODO -> we should keep track of this and retry, or aggregation will be broken + } + } + } + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { val eventId = event.eventId ?: return val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val newContent = content.newContent ?: return // ok, this is a replace - var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() - if (existing == null) { - Timber.v("###REPLACE creating new relation summary for $targetEventId") - existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) - } + val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId) // we have it val existingSummary = existing.editSummary @@ -228,7 +286,8 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId) val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) sum.key = it.key - sum.firstTimestamp = event.originServerTs ?: 0 // TODO how to maintain order? + sum.firstTimestamp = event.originServerTs + ?: 0 // TODO how to maintain order? sum.count = it.count eventSummary.reactionsSummary.add(sum) } else { @@ -251,8 +310,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( val relatedEventID = content.relatesTo.eventId val reactionEventId = event.eventId Timber.v("Reaction $reactionEventId relates to $relatedEventID") - val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst() - ?: EventAnnotationsSummaryEntity.create(realm, roomId, relatedEventID).apply { this.roomId = roomId } + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID) var sum = eventSummary.reactionsSummary.find { it.key == reaction } val txId = event.unsignedData?.transactionId @@ -374,4 +432,76 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( Timber.e("## Cannot find summary for key $reactionKey") } } + + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) + + val verifSummary = eventSummary.referencesSummaryEntity + ?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also { + eventSummary.referencesSummaryEntity = it + } + + val txId = event.unsignedData?.transactionId + + if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) { + // ok it has already been handled + } else { + ContentMapper.map(verifSummary.content)?.toModel() + var data = ContentMapper.map(verifSummary.content)?.toModel() + ?: ReferencesAggregatedContent(VerificationState.REQUEST.name) + // TODO ignore invalid messages? e.g a START after a CANCEL? + // i.e. never change state if already canceled/done + val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name } + val newState = when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + updateVerificationState(currentState, VerificationState.WAITING) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + updateVerificationState(currentState, VerificationState.WAITING) + } + EventType.KEY_VERIFICATION_READY -> { + updateVerificationState(currentState, VerificationState.WAITING) + } + EventType.KEY_VERIFICATION_KEY -> { + updateVerificationState(currentState, VerificationState.WAITING) + } + EventType.KEY_VERIFICATION_MAC -> { + updateVerificationState(currentState, VerificationState.WAITING) + } + EventType.KEY_VERIFICATION_CANCEL -> { + updateVerificationState(currentState, if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else VerificationState.CANCELED_BY_OTHER) + } + EventType.KEY_VERIFICATION_DONE -> { + updateVerificationState(currentState, VerificationState.DONE) + } + else -> VerificationState.REQUEST + } + + data = data.copy(verificationSummary = newState.name) + verifSummary.content = ContentMapper.map(data.toContent()) + } + + if (isLocalEcho) { + verifSummary.sourceLocalEcho.add(event.eventId) + } else { + verifSummary.sourceLocalEcho.remove(txId) + verifSummary.sourceEvents.add(event.eventId) + } + } + + private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState { + // Cancel is always prioritary ? + // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to + // consider as canceled + if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) { + return newState + } + // never move out of cancel + if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) { + return oldState + } + return newState + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 3aaa60680b..7ddcf3542d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -35,7 +35,6 @@ import javax.inject.Inject * For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. */ - internal class EventRelationsAggregationUpdater @Inject constructor( @SessionDatabase realmConfiguration: RealmConfiguration, @UserId private val userId: String, @@ -47,6 +46,14 @@ internal class EventRelationsAggregationUpdater @Inject constructor( EventType.MESSAGE, EventType.REDACTION, EventType.REACTION, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + // TODO Add ? + // EventType.KEY_VERIFICATION_READY, + EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 622ffbe2f0..9be63565fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -34,7 +34,13 @@ import im.vector.matrix.android.internal.session.room.timeline.EventContextRespo import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import im.vector.matrix.android.internal.session.room.typing.TypingBody import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query internal interface RoomAPI { @@ -60,9 +66,12 @@ internal interface RoomAPI { /** * Create a room. * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom + * Set all the timeouts to 1 minute, because if the server takes time to answer, we will not execute the + * create direct chat request if any * * @param param the creation room parameter */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom") fun createRoom(@Body param: CreateRoomParams): Call diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 5063bd3908..5ab1dc7cab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -56,6 +56,7 @@ internal class RoomSummaryUpdater @Inject constructor( // TODO: maybe allow user of SDK to give that list companion object { val PREVIEWABLE_TYPES = listOf( + // TODO filter message type (KEY_VERIFICATION_READY, etc.) EventType.MESSAGE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index b4f7bdcd55..f120130739 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -17,9 +17,11 @@ package im.vector.matrix.android.internal.session.room.create import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse +import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields @@ -49,12 +51,20 @@ internal class DefaultCreateRoomTask @Inject constructor( private val readMarkersTask: SetReadMarkersTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val crossSigningService: CrossSigningService, + private val deviceListManager: DeviceListManager, private val eventBus: EventBus ) : CreateRoomTask { override suspend fun execute(params: CreateRoomParams): String { + val createRoomParams = if (canEnableEncryption(params)) { + params.enableEncryptionWithAlgorithm() + } else { + params + } + val createRoomResponse = executeRequest(eventBus) { - apiCall = roomAPI.createRoom(params) + apiCall = roomAPI.createRoom(createRoomParams) } val roomId = createRoomResponse.roomId!! // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) @@ -66,13 +76,35 @@ internal class DefaultCreateRoomTask @Inject constructor( } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } - if (params.isDirect()) { - handleDirectChatCreation(params, roomId) + if (createRoomParams.isDirect()) { + handleDirectChatCreation(createRoomParams, roomId) } setReadMarkers(roomId) return roomId } + private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { + return params.enableEncryptionIfInvitedUsersSupportIt + && crossSigningService.isCrossSigningVerified() + && params.invite3pids.isNullOrEmpty() + && params.invitedUserIds?.isNotEmpty() == true + && params.invitedUserIds.let { userIds -> + val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) + + userIds.all { userId -> + keys.map[userId].let { deviceMap -> + if (deviceMap.isNullOrEmpty()) { + // A user has no device, so do not enable encryption + false + } else { + // Check that every user's device have at least one key + deviceMap.values.all { !it.keys.isNullOrEmpty() } + } + } + } + } + } + private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) { val otherUserId = params.getFirstInvitedUserId() ?: throw IllegalStateException("You can't create a direct room without an invitedUser") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 867c9eab1f..0310020b5a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -15,7 +15,6 @@ */ package im.vector.matrix.android.internal.session.room.relation -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.work.OneTimeWorkRequest @@ -46,15 +45,14 @@ import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber internal class DefaultRelationService @AssistedInject constructor( @Assisted private val roomId: String, - private val context: Context, @SessionId private val sessionId: String, + private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon, private val eventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, @@ -85,8 +83,7 @@ internal class DefaultRelationService @AssistedInject constructor( val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) .also { saveLocalEcho(it) } val sendRelationWork = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) - CancelableWork(context, sendRelationWork.id) + timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork) } else { Timber.w("Reaction already added") NoOpCancellable @@ -111,7 +108,7 @@ internal class DefaultRelationService @AssistedInject constructor( .also { saveLocalEcho(it) } val redactWork = createRedactEventWork(redactEvent, toRedact, null) - TimelineSendEventWorkCommon.postWork(context, roomId, redactWork) + timeLineSendEventWorkCommon.postWork(roomId, redactWork) } } } @@ -132,7 +129,7 @@ internal class DefaultRelationService @AssistedInject constructor( eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(redactWorkData, true) + return timeLineSendEventWorkCommon.createWork(redactWorkData, true) } override fun editTextMessage(targetEventId: String, @@ -146,12 +143,10 @@ internal class DefaultRelationService @AssistedInject constructor( return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) - TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - CancelableWork(context, encryptWork.id) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) } else { val workRequest = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - CancelableWork(context, workRequest.id) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) } } @@ -168,12 +163,10 @@ internal class DefaultRelationService @AssistedInject constructor( return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) - TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - CancelableWork(context, encryptWork.id) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) } else { val workRequest = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - CancelableWork(context, workRequest.id) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) } } @@ -194,12 +187,10 @@ internal class DefaultRelationService @AssistedInject constructor( return if (cryptoService.isRoomEncrypted(roomId)) { val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) val workRequest = createSendEventWork(event, false) - TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) - CancelableWork(context, encryptWork.id) + timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest) } else { val workRequest = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - CancelableWork(context, workRequest.id) + timeLineSendEventWorkCommon.postWork(roomId, workRequest) } } @@ -207,13 +198,13 @@ internal class DefaultRelationService @AssistedInject constructor( // Same parameter val params = EncryptEventWorker.Params(sessionId, roomId, event, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) - return TimelineSendEventWorkCommon.createWork(sendWorkData, true) + return timeLineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) + return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 5061b5f2c4..369ad660ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -36,13 +36,12 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker -import im.vector.matrix.android.internal.worker.WorkManagerUtil -import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.startChain import kotlinx.coroutines.launch @@ -55,7 +54,8 @@ private const val BACKOFF_DELAY = 10_000L internal class DefaultSendService @AssistedInject constructor( @Assisted private val roomId: String, - private val context: Context, + private val workManagerProvider: WorkManagerProvider, + private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon, @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, @@ -92,12 +92,10 @@ internal class DefaultSendService @AssistedInject constructor( Timber.v("Send event in encrypted room") val encryptWork = createEncryptEventWork(event, true) val sendWork = createSendEventWork(event, false) - TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork) - CancelableWork(context, encryptWork.id) + timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork) } else { val sendWork = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, sendWork) - CancelableWork(context, sendWork.id) + timelineSendEventWorkCommon.postWork(roomId, sendWork) } } @@ -110,8 +108,7 @@ internal class DefaultSendService @AssistedInject constructor( override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactWork = createRedactEventWork(event, reason) - TimelineSendEventWorkCommon.postWork(context, roomId, redactWork) - return CancelableWork(context, redactWork.id) + return timelineSendEventWorkCommon.postWork(roomId, redactWork) } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { @@ -163,123 +160,123 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun clearSendingQueue() { - TimelineSendEventWorkCommon.cancelAllWorks(context, roomId) - WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(UPLOAD_WORK)) + override fun clearSendingQueue() { + timelineSendEventWorkCommon.cancelAllWorks(roomId) + workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK)) - // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied - matrixOneTimeWorkRequestBuilder() - .build().let { - TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE) + // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .build().let { + timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE) - // need to clear also image sending queue - WorkManager.getInstance(context) - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) - .enqueue() - } - taskExecutor.executorScope.launch { - localEchoRepository.clearSendingQueue(roomId) - } - } - - override fun resendAllFailedMessages() { - taskExecutor.executorScope.launch { - val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) - eventsToResend.forEach { - sendEvent(it) + // need to clear also image sending queue + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) + .enqueue() + } + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) } - localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) } - } - override fun sendMedia(attachment: ContentAttachmentData): Cancelable { - // Create an event with the media file path - val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - createLocalEcho(it) - } - return internalSendMedia(event, attachment) - } - - private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): CancelableWork { - val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) - - val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) - val sendWork = createSendEventWork(localEcho, false) - - if (isRoomEncrypted) { - val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) - - val op: Operation = WorkManager.getInstance(context) - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(encryptWork) - .then(sendWork) - .enqueue() - op.result.addListener(Runnable { - if (op.result.isCancelled) { - Timber.e("CHAIN WAS CANCELLED") - } else if (op.state.value is Operation.State.FAILURE) { - Timber.e("CHAIN DID FAIL") + override fun resendAllFailedMessages() { + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) } - }, workerFutureListenerExecutor) - } else { - WorkManager.getInstance(context) - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(sendWork) - .enqueue() + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + } } - return CancelableWork(context, sendWork.id) - } - - private fun createLocalEcho(event: Event) { - localEchoEventFactory.createLocalEcho(event) - } - - private fun buildWorkName(identifier: String): String { - return "${roomId}_$identifier" - } - - private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(params) - - return matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) - .setInputData(sendWorkData) - .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - } - - private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) - } - - private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { - createLocalEcho(it) + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { + // Create an event with the media file path + val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { + createLocalEcho(it) + } + return internalSendMedia(event, attachment) } - val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) - val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return TimelineSendEventWorkCommon.createWork(redactWorkData, true) - } - private fun createUploadMediaWork(event: Event, - attachment: ContentAttachmentData, - isRoomEncrypted: Boolean, - startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) - val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) + private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable { + val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) - return matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) - .startChain(startChain) - .setInputData(uploadWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) + val sendWork = createSendEventWork(localEcho, false) + + if (isRoomEncrypted) { + val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) + + val op: Operation = workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(encryptWork) + .then(sendWork) + .enqueue() + op.result.addListener(Runnable { + if (op.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (op.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) + } else { + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(sendWork) + .enqueue() + } + + return CancelableWork(workManagerProvider.workManager, sendWork.id) + } + + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) + } + + private fun buildWorkName(identifier: String): String { + return "${roomId}_$identifier" + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, roomId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) + } + + private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { + val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { + createLocalEcho(it) + } + val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return timelineSendEventWorkCommon.createWork(redactWorkData, true) + } + + private fun createUploadMediaWork(event: Event, + attachment: ContentAttachmentData, + isRoomEncrypted: Boolean, + startChain: Boolean): OneTimeWorkRequest { + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) + val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(uploadWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } } -} 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/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 0db7377d4b..ecfd8cc0cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -37,6 +37,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageFileConten import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo @@ -304,6 +305,25 @@ internal class LocalEchoEventFactory @Inject constructor( ) } + fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List): Event { + val localID = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localID, + type = EventType.MESSAGE, + content = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = fromDevice, + toUserId = toUserId, + timestamp = System.currentTimeMillis(), + methods = methods + ).toContent(), + unsignedData = UnsignedData(age = null, transactionId = localID) + ) + } + private fun dummyOriginServerTs(): Long { return System.currentTimeMillis() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt index bc0f413e92..c924e891c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoRepository.kt @@ -24,17 +24,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.nextId import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity -import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper @@ -45,7 +42,6 @@ import org.greenrobot.eventbus.EventBus import timber.log.Timber import java.lang.IllegalStateException import javax.inject.Inject -import kotlin.random.Random internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy, private val roomSummaryUpdater: RoomSummaryUpdater, @@ -84,9 +80,7 @@ internal class LocalEchoRepository @Inject constructor(private val monarchy: Mon suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { monarchy.awaitTransaction { realm -> TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt index 4c45ba0a4d..60d1a217a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt @@ -20,15 +20,14 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) { - suspend fun updateSendState(eventId: String, sendState: SendState) { + fun updateSendState(eventId: String, sendState: SendState) { Timber.v("Update local state of $eventId to ${sendState.name}") - monarchy.awaitTransaction { realm -> + monarchy.writeAsync { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 3215662ba2..3d038a0c82 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,8 +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.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.failure.shouldBeRetried 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.room.send.SendState @@ -33,7 +32,8 @@ import im.vector.matrix.android.internal.worker.getSessionComponent import org.greenrobot.eventbus.EventBus import javax.inject.Inject -internal class SendEventWorker constructor(context: Context, params: WorkerParameters) +internal class SendEventWorker(context: Context, + params: WorkerParameters) : CoroutineWorker(context, params) { @JsonClass(generateAdapter = true) @@ -79,11 +79,6 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam } } - private fun Throwable.shouldBeRetried(): Boolean { - return this is Failure.NetworkConnection - || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) - } - private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { localEchoUpdater.updateSendState(eventId, SendState.SENDING) executeRequest(eventBus) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index f6f894d860..ff3cedf044 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -15,51 +15,54 @@ */ package im.vector.matrix.android.internal.session.room.timeline -import android.content.Context import androidx.work.* -import im.vector.matrix.android.internal.worker.WorkManagerUtil -import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.startChain import java.util.concurrent.TimeUnit - -private const val SEND_WORK = "SEND_WORK" -private const val BACKOFF_DELAY = 10_000L +import javax.inject.Inject /** * Helper class for sending event related works. * All send event from a room are using the same workchain, in order to ensure order. - * WorkRequest must always return success (even if server error, in this case marking the event as failed to send) - * , if not the chain will be doomed in failed state. - * + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send), + * if not the chain will be doomed in failed state. */ -internal object TimelineSendEventWorkCommon { +internal class TimelineSendEventWorkCommon @Inject constructor( + private val workManagerProvider: WorkManagerProvider +) { - fun postSequentialWorks(context: Context, roomId: String, vararg workRequests: OneTimeWorkRequest) { - when { - workRequests.isEmpty() -> return - workRequests.size == 1 -> postWork(context, roomId, workRequests.first()) + fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest): Cancelable { + return when { + workRequests.isEmpty() -> NoOpCancellable + workRequests.size == 1 -> postWork(roomId, workRequests.first()) else -> { val firstWork = workRequests.first() - var continuation = WorkManager.getInstance(context) + var continuation = workManagerProvider.workManager .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork) for (i in 1 until workRequests.size) { val workRequest = workRequests[i] continuation = continuation.then(workRequest) } continuation.enqueue() + CancelableWork(workManagerProvider.workManager, firstWork.id) } } } - fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) { - WorkManager.getInstance(context) + fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + workManagerProvider.workManager .beginUniqueWork(buildWorkName(roomId), policy, workRequest) .enqueue() + + return CancelableWork(workManagerProvider.workManager, workRequest.id) } inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { - return matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) .startChain(startChain) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) @@ -70,7 +73,13 @@ internal object TimelineSendEventWorkCommon { return "${roomId}_$SEND_WORK" } - fun cancelAllWorks(context: Context, roomId: String) { - WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(roomId)) + fun cancelAllWorks(roomId: String) { + workManagerProvider.workManager + .cancelUniqueWork(buildWorkName(roomId)) + } + + companion object { + private const val SEND_WORK = "SEND_WORK" + private const val BACKOFF_DELAY = 10_000L } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index f2439d57ca..4bd9c4be79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -26,10 +26,8 @@ import im.vector.matrix.android.internal.database.helper.addStateEvent import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.merge -import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity @@ -39,7 +37,6 @@ import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findAllIncludingEvents import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.getOrCreate -import im.vector.matrix.android.internal.database.query.getOrNull import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index e6913f8b54..05a2324047 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.signout -import android.content.Context import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -29,7 +28,6 @@ import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.task.Task -import im.vector.matrix.android.internal.worker.WorkManagerUtil import io.realm.Realm import io.realm.RealmConfiguration import org.greenrobot.eventbus.EventBus @@ -45,7 +43,7 @@ internal interface SignOutTask : Task { } internal class DefaultSignOutTask @Inject constructor( - private val context: Context, + private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val signOutAPI: SignOutAPI, private val sessionManager: SessionManager, @@ -87,7 +85,7 @@ internal class DefaultSignOutTask @Inject constructor( sessionManager.releaseSession(sessionId) Timber.d("SignOut: cancel pending works...") - WorkManagerUtil.cancelAllWorks(context) + workManagerProvider.cancelAllWorks() Timber.d("SignOut: delete session params...") sessionParamsStore.delete(sessionId) 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..bda3623078 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 verificationService: DefaultVerificationService) { fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) { val total = toDevice.events?.size ?: 0 @@ -44,7 +44,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: && event.getClearContent()?.toModel()?.type == "m.bad.encrypted") { Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { - sasVerificationService.onToDeviceEvent(event) + verificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index b8ac0318fd..ab2c9a48a6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -85,14 +85,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle // PRIVATE METHODS ***************************************************************************** private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { + val syncLocalTimeStampMillis = System.currentTimeMillis() val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { - handleJoinedRoom(realm, it.key, it.value, isInitialSync) + handleJoinedRoom(realm, it.key, it.value, isInitialSync, syncLocalTimeStampMillis) } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { - handleInvitedRoom(realm, it.key, it.value) + handleInvitedRoom(realm, it.key, it.value, syncLocalTimeStampMillis) } is HandlingStrategy.LEFT -> { @@ -107,7 +108,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun handleJoinedRoom(realm: Realm, roomId: String, roomSync: RoomSync, - isInitialSync: Boolean): RoomEntity { + isInitialSync: Boolean, + syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle join sync for room $roomId") var ephemeralResult: EphemeralResult? = null @@ -151,7 +153,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, - roomSync.timeline.limited + roomSync.timeline.limited, + syncLocalTimestampMillis ) roomEntity.addOrUpdate(chunkEntity) } @@ -174,12 +177,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private fun handleInvitedRoom(realm: Realm, roomId: String, - roomSync: InvitedRoomSync): RoomEntity { + roomSync: InvitedRoomSync, + syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { - val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events) + val chunkEntity = handleTimelineEvents(realm, roomId, roomEntity, roomSync.inviteState.events, syncLocalTimestampMillis = syncLocalTimestampMillis) roomEntity.addOrUpdate(chunkEntity) } val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { @@ -205,7 +209,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle roomEntity: RoomEntity, eventList: List, prevToken: String? = null, - isLimited: Boolean = true): ChunkEntity { + isLimited: Boolean = true, + syncLocalTimestampMillis: Long): ChunkEntity { val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk @@ -223,7 +228,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle continue } eventIds.add(event.eventId) - val eventEntity = event.toEntity(roomId, SendState.SYNCED).let { + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).let { realm.copyToRealmOrUpdate(it) } if (event.isStateEvent() && event.stateKey != null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt index 3637cc624f..c844db8d33 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncWorker.kt @@ -16,15 +16,17 @@ package im.vector.matrix.android.internal.session.sync.job import android.content.Context -import androidx.work.* +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.failure.isTokenError +import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.worker.SessionWorkerParams -import im.vector.matrix.android.internal.worker.WorkManagerUtil -import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber @@ -75,30 +77,33 @@ internal class SyncWorker(context: Context, companion object { - const val BG_SYNC_WORK_NAME = "BG_SYNCP" + private const val BG_SYNC_WORK_NAME = "BG_SYNCP" - fun requireBackgroundSync(context: Context, sessionId: String, serverTimeout: Long = 0) { + fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) - val workRequest = matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .setInputData(data) .build() - WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) } - fun automaticallyBackgroundSync(context: Context, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { + fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) - val workRequest = matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerUtil.workConstraints) + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) .setInputData(data) .setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) .build() - WorkManager.getInstance(context).enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + workManagerProvider.workManager + .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) } - fun stopAnyBackgroundSync(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(BG_SYNC_WORK_NAME) + fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { + workManagerProvider.workManager + .cancelUniqueWork(BG_SYNC_WORK_NAME) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index 705073fde5..ec78173190 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -62,5 +62,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers TaskThread.IO -> coroutineDispatchers.io TaskThread.CALLER -> EmptyCoroutineContext TaskThread.CRYPTO -> coroutineDispatchers.crypto + TaskThread.DM_VERIF -> coroutineDispatchers.dmVerif } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt index c04e9fbce6..8da15fc3d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt @@ -21,5 +21,6 @@ internal enum class TaskThread { COMPUTATION, IO, CALLER, - CRYPTO + CRYPTO, + DM_VERIF } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt index bff20a80e7..cba104ebe8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CancelableWork.kt @@ -16,15 +16,14 @@ package im.vector.matrix.android.internal.util -import android.content.Context import androidx.work.WorkManager import im.vector.matrix.android.api.util.Cancelable -import java.util.UUID +import java.util.* -internal class CancelableWork(private val context: Context, +internal class CancelableWork(private val workManager: WorkManager, private val workId: UUID) : Cancelable { override fun cancel() { - WorkManager.getInstance(context).cancelWorkById(workId) + workManager.cancelWorkById(workId) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt index d15389f703..503a4aa910 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt @@ -22,5 +22,6 @@ internal data class MatrixCoroutineDispatchers( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val main: CoroutineDispatcher, - val crypto: CoroutineDispatcher + val crypto: CoroutineDispatcher, + val dmVerif: CoroutineDispatcher ) 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/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt deleted file mode 100644 index 27ec28dcac..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt +++ /dev/null @@ -1,49 +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.matrix.android.internal.worker - -import android.content.Context -import androidx.work.* - -// TODO Multiaccount -internal object WorkManagerUtil { - private const val MATRIX_SDK_TAG = "MatrixSDK" - - /** - * Default constraints: connected network - */ - val workConstraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - /** - * Create a OneTimeWorkRequestBuilder, with the Matrix SDK tag - */ - inline fun matrixOneTimeWorkRequestBuilder() = - OneTimeWorkRequestBuilder() - .addTag(MATRIX_SDK_TAG) - - /** - * Cancel all works instantiated by the Matrix SDK and not those from the SDK client - */ - fun cancelAllWorks(context: Context) { - WorkManager.getInstance(context).also { - it.cancelAllWorkByTag(MATRIX_SDK_TAG) - it.pruneWork() - } - } -} diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index d030b857cf..dae8271d66 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -4,4 +4,5 @@ %1$s turned on end-to-end encryption. %1$s turned on end-to-end encryption (unrecognised algorithm %2$s). + %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. 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..2dd0649be1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,246 @@ +/* + * 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.shouldBe +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", + requestId = "\$azertyazerty", + action = QrCodeData.ACTION_VERIFY, + keys = mapOf( + "1" to "abcdef", + "2" to "ghijql" + ), + sharedSecret = "sharedSecret", + otherUserKey = "otherUserKey", + otherDeviceKey = "otherDeviceKey" + ) + + 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" + + "&other_device_key=otherDeviceKey" + + @Test + fun testNominalCase() { + val url = basicQrCodeData.toUrl() + + url shouldBeEqualTo basicUrl + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit:matrix.org" + decodedData.requestId shouldBeEqualTo "\$azertyazerty" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey") + decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey") + } + + @Test + fun testSlashCase() { + val url = basicQrCodeData + .copy( + userId = "@benoit/foo:matrix.org", + requestId = "\$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.requestId shouldBeEqualTo "\$azertyazerty/bar" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey!! shouldBeEqualTo "otherUserKey" + decodedData.otherDeviceKey!! shouldBeEqualTo "otherDeviceKey" + } + + @Test + fun testNoOtherUserKey() { + val url = basicQrCodeData + .copy( + otherUserKey = null + ) + .toUrl() + + url shouldBeEqualTo basicUrl + .replace("&other_user_key=otherUserKey", "") + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit:matrix.org" + decodedData.requestId shouldBeEqualTo "\$azertyazerty" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey shouldBe null + decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey") + } + + @Test + fun testNoOtherDeviceKey() { + val url = basicQrCodeData + .copy( + otherDeviceKey = null + ) + .toUrl() + + url shouldBeEqualTo basicUrl + .replace("&other_device_key=otherDeviceKey", "") + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit:matrix.org" + decodedData.requestId shouldBeEqualTo "\$azertyazerty" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey") + decodedData.otherDeviceKey shouldBe null + } + + @Test + fun testUrlCharInKeys() { + val url = basicQrCodeData + .copy( + keys = mapOf( + "/=" to "abcdef", + "&?" to "ghijql" + ) + ) + .toUrl() + + url shouldBeEqualTo basicUrl + .replace("key_1=abcdef", "key_%2F%3D=abcdef") + .replace("key_2=ghijql", "key_%26%3F=ghijql") + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.keys["/="]?.shouldBeEqualTo("abcdef") + decodedData.keys["&&"]?.shouldBeEqualTo("ghijql") + } + + @Test + fun testMissingActionCase() { + basicUrl.replace("&action=verify", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testEmptyActionCase() { + basicUrl.replace("&action=verify", "&action=") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testOtherActionCase() { + basicUrl.replace("&action=verify", "&action=confirm") + .toQrCodeData() + ?.action + ?.shouldBeEqualTo("confirm") + } + + @Test + fun testMissingRequestId() { + basicUrl.replace("request=%24azertyazerty", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testEmptyRequestId() { + basicUrl.replace("request=%24azertyazerty", "request=") + .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 testEmptySecret() { + basicUrl.replace("&secret=sharedSecret", "&secret=") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testSelfSigning() { + // request is not an eventId in this case + val url = "https://matrix.to/#/@benoit0815:matrix.org" + + "?request=local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" + + "&action=verify" + + "&key_utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs=utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs" + + "&key_YSOXZVBXIZ=F0XWqgUePgwm5HMYG3yhBNneHmscrAxxlooLHjy8YQc" + + "&secret=LYVcEQmfdorbJ3vbQnq7nbNZc%2BGmDxUen1rByV9hRM4" + + "&other_device_key=eGoUqZqAroCYpjp7FLGIkTEzYHBFED4uUAfJ267gqQQ" + + url.toQrCodeData()!!.requestId shouldBeEqualTo "local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" + } +} diff --git a/vector/build.gradle b/vector/build.gradle index bb2edea5d5..fca82f935e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 14 +ext.versionMinor = 15 ext.versionPatch = 0 static def getGitTimestamp() { @@ -368,6 +368,11 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + // QR-code + // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 + implementation 'com.google.zxing:core:3.3.3' + 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 a0d4402767..3207ab257a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + - @@ -126,13 +124,16 @@ - + + 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/animations/AppBarStateChangeListener.kt b/vector/src/main/java/im/vector/riotx/core/animations/AppBarStateChangeListener.kt
    index b739f52bf3..bbc78d7937 100644
    --- a/vector/src/main/java/im/vector/riotx/core/animations/AppBarStateChangeListener.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/animations/AppBarStateChangeListener.kt
    @@ -27,7 +27,8 @@ abstract class AppBarStateChangeListener : OnOffsetChangedListener {
             EXPANDED, COLLAPSED, IDLE
         }
     
    -    private var currentState = State.IDLE
    +    var currentState = State.IDLE
    +        private set
     
         override fun onOffsetChanged(appBarLayout: AppBarLayout, i: Int) {
             currentState = if (i == 0) {
    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 4fdd2f1f73..6031a9f6cf 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
    @@ -25,10 +25,11 @@ import dagger.multibindings.IntoMap
     import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
     import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
     import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
    -import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment
    -import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment
    -import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment
    -import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment
    +import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
    +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.qrconfirmation.VerificationQrScannedByOtherFragment
    +import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
     import im.vector.riotx.features.grouplist.GroupListFragment
     import im.vector.riotx.features.home.HomeDetailFragment
     import im.vector.riotx.features.home.HomeDrawerFragment
    @@ -49,6 +50,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,15 +58,19 @@ 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
     import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
     import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
    +import im.vector.riotx.features.settings.VectorSettingsLabsFragment
     import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
     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
    @@ -72,7 +78,6 @@ import im.vector.riotx.features.signout.soft.SoftLogoutFragment
     
     @Module
     interface FragmentModule {
    -
         /**
          * Fragments with @IntoMap will be injected by this factory
          */
    @@ -229,6 +234,11 @@ interface FragmentModule {
         @FragmentKey(VectorSettingsNotificationPreferenceFragment::class)
         fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment
     
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VectorSettingsLabsFragment::class)
    +    fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment
    +
         @Binds
         @IntoMap
         @FragmentKey(VectorSettingsPreferencesFragment::class)
    @@ -254,26 +264,6 @@ interface FragmentModule {
         @FragmentKey(VectorSettingsDevicesFragment::class)
         fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
     
    -    @Binds
    -    @IntoMap
    -    @FragmentKey(SASVerificationIncomingFragment::class)
    -    fun bindSASVerificationIncomingFragment(fragment: SASVerificationIncomingFragment): Fragment
    -
    -    @Binds
    -    @IntoMap
    -    @FragmentKey(SASVerificationShortCodeFragment::class)
    -    fun bindSASVerificationShortCodeFragment(fragment: SASVerificationShortCodeFragment): Fragment
    -
    -    @Binds
    -    @IntoMap
    -    @FragmentKey(SASVerificationVerifiedFragment::class)
    -    fun bindSASVerificationVerifiedFragment(fragment: SASVerificationVerifiedFragment): Fragment
    -
    -    @Binds
    -    @IntoMap
    -    @FragmentKey(SASVerificationStartFragment::class)
    -    fun bindSASVerificationStartFragment(fragment: SASVerificationStartFragment): Fragment
    -
         @Binds
         @IntoMap
         @FragmentKey(PublicRoomsFragment::class)
    @@ -313,4 +303,49 @@ interface FragmentModule {
         @IntoMap
         @FragmentKey(SoftLogoutFragment::class)
         fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VerificationRequestFragment::class)
    +    fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VerificationChooseMethodFragment::class)
    +    fun bindVerificationChooseMethodFragment(fragment: VerificationChooseMethodFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VerificationEmojiCodeFragment::class)
    +    fun bindVerificationEmojiCodeFragment(fragment: VerificationEmojiCodeFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(VerificationQrScannedByOtherFragment::class)
    +    fun bindVerificationQrScannedByOtherFragment(fragment: VerificationQrScannedByOtherFragment): Fragment
    +
    +    @Binds
    +    @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 2463b577a6..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,10 +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
    @@ -42,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
    @@ -49,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
    @@ -134,8 +139,18 @@ interface ScreenComponent {
     
         fun inject(activity: SoftLogoutActivity)
     
    +    fun inject(verificationBottomSheet: VerificationBottomSheet)
    +
         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/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    index 5d4288f4b8..4bb0adb9f0 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt
    @@ -27,7 +27,6 @@ import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromK
     import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
     import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
     import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
    -import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
     import im.vector.riotx.features.home.HomeSharedActionViewModel
     import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
     import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
    @@ -61,11 +60,6 @@ interface ViewModelModule {
         @ViewModelKey(EmojiChooserViewModel::class)
         fun bindEmojiChooserViewModel(viewModel: EmojiChooserViewModel): ViewModel
     
    -    @Binds
    -    @IntoMap
    -    @ViewModelKey(SasVerificationViewModel::class)
    -    fun bindSasVerificationViewModel(viewModel: SasVerificationViewModel): ViewModel
    -
         @Binds
         @IntoMap
         @ViewModelKey(KeysBackupRestoreFromKeyViewModel::class)
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/ErrorWithRetryItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/ErrorWithRetryItem.kt
    index e6336e5753..25bf2f514a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/ErrorWithRetryItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/ErrorWithRetryItem.kt
    @@ -18,6 +18,7 @@ package im.vector.riotx.core.epoxy
     
     import android.widget.Button
     import android.widget.TextView
    +import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
    @@ -33,6 +34,7 @@ abstract class ErrorWithRetryItem : VectorEpoxyModel(
     
         override fun bind(holder: Holder) {
             holder.textView.text = text
    +        holder.buttonView.isVisible = listener != null
             holder.buttonView.setOnClickListener { listener?.invoke() }
         }
     
    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..27586fcfde 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
    @@ -16,11 +16,13 @@
     
     package im.vector.riotx.core.epoxy.profiles
     
    +import android.content.res.ColorStateList
     import android.view.View
     import android.widget.ImageView
     import android.widget.TextView
     import androidx.core.content.ContextCompat
     import androidx.core.view.isVisible
    +import androidx.core.widget.ImageViewCompat
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
    @@ -38,10 +40,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
     
    @@ -62,10 +70,24 @@ abstract class ProfileActionItem : VectorEpoxyModel()
             holder.subtitle.setTextOrHide(subtitle)
             if (iconRes != 0) {
                 holder.icon.setImageResource(iconRes)
    +            ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(tintColor))
                 holder.icon.isVisible = true
             } else {
                 holder.icon.isVisible = false
             }
    +
    +        if (editableRes != 0) {
    +            val tintColorSecondary = if (destructive) {
    +                tintColor
    +            } else {
    +                ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary)
    +            }
    +            holder.editable.setImageResource(editableRes)
    +            ImageViewCompat.setImageTintList(holder.editable, ColorStateList.valueOf(tintColorSecondary))
    +            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..beea3ca620 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,7 @@ fun EpoxyController.buildProfileAction(
             id("action_$id")
             subtitle(subtitle)
             editable(editable)
    +        editableRes?.let { editableRes(editableRes) }
             destructive(destructive)
             title(title)
             listener { _ ->
    diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    index 4fe65748ce..5d9a89be34 100644
    --- a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt
    @@ -22,11 +22,13 @@ import android.widget.ImageView
     import android.widget.TextView
     import com.airbnb.epoxy.EpoxyAttribute
     import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
     import im.vector.matrix.android.api.util.MatrixItem
     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.crypto.util.toImageRes
     import im.vector.riotx.features.home.AvatarRenderer
     
     @EpoxyModelClass(layout = R.layout.item_profile_matrix_item)
    @@ -34,6 +36,7 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
     
         @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
         @EpoxyAttribute lateinit var matrixItem: MatrixItem
    +    @EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
         @EpoxyAttribute var clickListener: View.OnClickListener? = null
     
         override fun bind(holder: Holder) {
    @@ -43,11 +46,13 @@ abstract class ProfileMatrixItem : VectorEpoxyModel()
             holder.titleView.text = bestName
             holder.subtitleView.setTextOrHide(matrixId)
             avatarRenderer.render(matrixItem, holder.avatarImageView)
    +        holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
         }
     
         class Holder : VectorEpoxyHolder() {
             val titleView by bind(R.id.matrixItemTitle)
             val subtitleView by bind(R.id.matrixItemSubtitle)
             val avatarImageView by bind(R.id.matrixItemAvatar)
    +        val avatarDecorationImageView by bind(R.id.matrixItemAvatarDecoration)
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
    index 3d247e149c..51f3ce611a 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt
    @@ -28,12 +28,17 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
                                    itemAnimator: RecyclerView.ItemAnimator? = null,
                                    viewPool: RecyclerView.RecycledViewPool? = null,
                                    showDivider: Boolean = false,
    -                               hasFixedSize: Boolean = true) {
    +                               hasFixedSize: Boolean = true,
    +                               disableItemAnimation: Boolean = false) {
         layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
             recycleChildrenOnDetach = viewPool != null
         }
         setRecycledViewPool(viewPool)
    -    itemAnimator?.let { this.itemAnimator = it }
    +    if (disableItemAnimation) {
    +        this.itemAnimator = null
    +    } else {
    +        itemAnimator?.let { this.itemAnimator = it }
    +    }
         if (showDivider) {
             addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
         }
    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 d57b11d93d..2649662ee5 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
    @@ -101,9 +101,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
         private lateinit var sessionListener: SessionListener
         protected lateinit var bugReporter: BugReporter
         lateinit var rageShake: RageShake
    +
    +    lateinit var navigator: Navigator
             private set
    -    protected lateinit var navigator: Navigator
         private lateinit var fragmentFactory: FragmentFactory
    +
         private lateinit var activeSessionHolder: ActiveSessionHolder
         private lateinit var vectorPreferences: VectorPreferences
     
    @@ -209,8 +211,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
                     handleInvalidToken(globalError)
                 is GlobalError.ConsentNotGivenError ->
                     consentNotGivenHelper.displayDialog(globalError.consentUri,
    -                                                    activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host
    -                                                    ?: "")
    +                        activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host
    +                                ?: "")
             }
         }
     
    @@ -223,11 +225,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/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    index b24c685dba..9b86782c42 100644
    --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt
    @@ -19,9 +19,15 @@ import android.app.Dialog
     import android.content.Context
     import android.os.Bundle
     import android.os.Parcelable
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
     import android.widget.FrameLayout
     import androidx.annotation.CallSuper
    +import androidx.annotation.LayoutRes
     import androidx.lifecycle.ViewModelProvider
    +import butterknife.ButterKnife
    +import butterknife.Unbinder
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.MvRxView
     import com.airbnb.mvrx.MvRxViewId
    @@ -42,6 +48,15 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
         final override val mvrxViewId: String by mvrxViewIdProperty
         private lateinit var screenComponent: ScreenComponent
     
    +    /* ==========================================================================================
    +     * View
    +     * ========================================================================================== */
    +
    +    @LayoutRes
    +    abstract fun getLayoutResId(): Int
    +
    +    private var unBinder: Unbinder? = null
    +
         /* ==========================================================================================
          * View model
          * ========================================================================================== */
    @@ -66,6 +81,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
     
         open val showExpanded = false
     
    +    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    +        val view = inflater.inflate(getLayoutResId(), container, false)
    +        unBinder = ButterKnife.bind(this, view)
    +        return view
    +    }
    +
    +    override fun onDestroyView() {
    +        super.onDestroyView()
    +        unBinder?.unbind()
    +        unBinder = null
    +    }
    +
         override fun onAttach(context: Context) {
             screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
             viewModelFactory = screenComponent.viewModelFactory()
    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/core/utils/SpannableUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt
    new file mode 100644
    index 0000000000..6536da73f5
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt
    @@ -0,0 +1,58 @@
    +/*
    + * 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.utils
    +
    +import android.text.Spannable
    +import android.text.style.BulletSpan
    +import android.text.style.ClickableSpan
    +import android.text.style.ForegroundColorSpan
    +import android.text.style.StyleSpan
    +import androidx.annotation.ColorInt
    +import me.gujun.android.span.Span
    +
    +fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable {
    +    if (match.isEmpty()) return this
    +    indexOf(match).takeIf { it != -1 }?.let { start ->
    +        this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    +    }
    +    return this
    +}
    +
    +fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable {
    +    if (match.isEmpty()) return this
    +    indexOf(match).takeIf { it != -1 }?.let { start ->
    +        this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    +    }
    +    return this
    +}
    +
    +fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable {
    +    if (match.isEmpty()) return this
    +    indexOf(match).takeIf { it != -1 }?.let { start ->
    +        this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    +    }
    +    return this
    +}
    +
    +fun Span.bullet(text: CharSequence = "",
    +                init: Span.() -> Unit = {}): Span = apply {
    +    append(Span(parent = this).apply {
    +        this.text = text
    +        this.spans.add(BulletSpan())
    +        init()
    +        build()
    +    })
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
    index 26ecd25178..c714be7650 100644
    --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
    @@ -189,7 +189,7 @@ class MainActivity : VectorBaseActivity() {
                     // The homeserver has invalidated the token, with a soft logout
                     SoftLogoutActivity.newIntent(this)
                 args.isUserLoggedOut             ->
    -                // the homeserver has invalidated the token (password changed, device deleted, other security reason
    +                // the homeserver has invalidated the token (password changed, device deleted, other security reasons)
                     SignedOutActivity.newIntent(this)
                 sessionHolder.hasActiveSession() ->
                     // We have a session.
    diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
    index 6151ae0d66..5c96b7c93c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt
    @@ -40,7 +40,10 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
         RAINBOW("/rainbow", "", R.string.command_description_rainbow),
         RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote),
         CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
    -    SPOILER("/spoiler", "", R.string.command_description_spoiler);
    +    SPOILER("/spoiler", "", R.string.command_description_spoiler),
    +    SHRUG("/shrug", "", R.string.command_description_shrug),
    +    // TODO temporary command
    +    VERIFY_USER("/verify", "", R.string.command_description_verify);
     
         val length
             get() = command.length + 1
    diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    index 58671df539..d4f5010d7e 100644
    --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt
    @@ -254,6 +254,17 @@ object CommandParser {
     
                         ParsedCommand.SendSpoiler(message)
                     }
    +                Command.SHRUG.command                  -> {
    +                    val message = textMessage.substring(Command.SHRUG.command.length).trim()
    +
    +                    ParsedCommand.SendShrug(message)
    +                }
    +
    +                Command.VERIFY_USER.command            -> {
    +                    val message = textMessage.substring(Command.VERIFY_USER.command.length).trim()
    +
    +                    ParsedCommand.VerifyUser(message)
    +                }
                     else                                   -> {
                         // Unknown command
                         ParsedCommand.ErrorUnknownSlashCommand(slashCommand)
    diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    index c43b78d71c..dd9fe32e09 100644
    --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt
    @@ -48,4 +48,6 @@ sealed class ParsedCommand {
         class SetMarkdown(val enable: Boolean) : ParsedCommand()
         object ClearScalarToken : ParsedCommand()
         class SendSpoiler(val message: String) : ParsedCommand()
    +    class SendShrug(val message: CharSequence) : ParsedCommand()
    +    class VerifyUser(val userId: String) : ParsedCommand()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    index c3a66dce1e..1fcc8d443b 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt
    @@ -98,10 +98,9 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
         }
     
         private fun setupRecyclerView() {
    -        // Don't activate animation as we might have way to much item animation when filtering
    -        recyclerView.itemAnimator = null
             knownUsersController.callback = this
    -        recyclerView.configureWith(knownUsersController)
    +        // Don't activate animation as we might have way to much item animation when filtering
    +        recyclerView.configureWith(knownUsersController, disableItemAnimation = true)
         }
     
         private fun setupFilterView() {
    diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    index fd56aacee4..6456acaf22 100644
    --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt
    @@ -92,13 +92,12 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
         }
     
         private fun createRoomAndInviteSelectedUsers() = withState { currentState ->
    -        val isDirect = currentState.selectedUsers.size == 1
    -        val roomParams = CreateRoomParams().apply {
    -            invitedUserIds = ArrayList(currentState.selectedUsers.map { it.userId })
    -            if (isDirect) {
    -                setDirectMessage()
    -            }
    -        }
    +        val roomParams = CreateRoomParams(
    +                invitedUserIds = currentState.selectedUsers.map { it.userId }
    +        )
    +                .setDirectMessage()
    +                .enableEncryptionIfInvitedUsersSupportIt()
    +
             session.rx()
                     .createRoom(roomParams)
                     .execute {
    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 f5db16c8ee..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,23 +23,23 @@ 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
     import im.vector.riotx.R
    -import im.vector.riotx.features.crypto.verification.SASVerificationActivity
     import im.vector.riotx.features.popup.PopupAlertManager
     import timber.log.Timber
     import java.text.DateFormat
     import java.text.SimpleDateFormat
    -import java.util.Locale
    -import java.util.Date
    +import java.util.*
     import javax.inject.Inject
     import javax.inject.Singleton
     import kotlin.collections.ArrayList
    @@ -56,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>()
     
    @@ -64,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
         }
    @@ -90,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)
    @@ -100,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) {
    @@ -111,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 {
    @@ -145,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?
    @@ -182,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
    @@ -190,23 +190,24 @@ 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)
             }
     
    -        alert.addButton(
    -                context.getString(R.string.start_verification_short_label),
    -                Runnable {
    -                    alert.weakCurrentActivity?.get()?.let {
    -                        val intent = SASVerificationActivity.outgoingIntent(it,
    -                                session?.myUserId ?: "",
    -                                userId, deviceId)
    -                        it.startActivity(intent)
    -                    }
    -                },
    -                false
    -        )
    +        // TODO send to the new profile page
    +//        alert.addButton(
    +//                context.getString(R.string.start_verification_short_label),
    +//                Runnable {
    +//                    alert.weakCurrentActivity?.get()?.let {
    +//                        val intent = SASVerificationActivity.outgoingIntent(it,
    +//                                session?.myUserId ?: "",
    +//                                userId, deviceId)
    +//                        it.startActivity(intent)
    +//                    }
    +//                },
    +//                false
    +//        )
     
             alert.addButton(context.getString(R.string.share_without_verifying_short_label), Runnable {
                 shareAllSessions(mappingKey)
    @@ -249,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
    @@ -257,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/util/Extensions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/util/Extensions.kt
    new file mode 100644
    index 0000000000..f4885498c3
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/util/Extensions.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.riotx.features.crypto.util
    +
    +import androidx.annotation.DrawableRes
    +import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
    +import im.vector.riotx.R
    +import im.vector.riotx.core.extensions.exhaustive
    +
    +@DrawableRes
    +fun RoomEncryptionTrustLevel?.toImageRes(): Int {
    +    return when (this) {
    +        null                             -> 0
    +        RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
    +        RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
    +        RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
    +    }.exhaustive
    +}
    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
    new file mode 100644
    index 0000000000..fae7037403
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.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.riotx.features.crypto.verification
    +
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
    +
    +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 22ccec534a..0fbbccee8a 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,11 +17,16 @@ 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.VerificationTransaction
    +import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
    +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.R
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +import im.vector.riotx.features.home.room.detail.RoomDetailActivity
    +import im.vector.riotx.features.home.room.detail.RoomDetailArgs
     import im.vector.riotx.features.popup.PopupAlertManager
    +import im.vector.riotx.features.themes.ThemeUtils
     import javax.inject.Inject
     import javax.inject.Singleton
     
    @@ -29,41 +34,52 @@ 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
    +        val uid = "kvr_${tx.transactionId}"
             when (tx.state) {
    -            SasVerificationTxState.OnStarted -> {
    +            is 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}",
    +                        uid,
                             context.getString(R.string.sas_incoming_request_notif_title),
                             context.getString(R.string.sas_incoming_request_notif_content, name),
    -                        R.drawable.shield)
    +                        R.drawable.shield,
    +                        shouldBeDisplayedIn = { activity ->
    +                            if (activity is VectorBaseActivity) {
    +                                // TODO a bit too hugly :/
    +                                activity.supportFragmentManager.findFragmentByTag(VerificationBottomSheet.WAITING_SELF_VERIF_TAG)?.let {
    +                                    false.also {
    +                                        PopupAlertManager.cancelAlert(uid)
    +                                    }
    +                                } ?: true
    +                            } else true
    +                        })
                             .apply {
                                 contentAction = Runnable {
    -                                val intent = SASVerificationActivity.incomingIntent(context,
    -                                        session?.myUserId  ?: "",
    -                                        tx.otherUserId,
    -                                        tx.transactionId)
    -                                weakCurrentActivity?.get()?.startActivity(intent)
    +                                (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                    it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
    +                                }
                                 }
                                 dismissedAction = Runnable {
                                     tx.cancel()
    @@ -77,11 +93,9 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
                                 addButton(
                                         context.getString(R.string.action_open),
                                         Runnable {
    -                                        val intent = SASVerificationActivity.incomingIntent(context,
    -                                                session?.myUserId ?: "",
    -                                                tx.otherUserId,
    -                                                tx.transactionId)
    -                                        weakCurrentActivity?.get()?.startActivity(intent)
    +                                        (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                            it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId)
    +                                        }
                                         }
                                 )
                                 // 10mn expiration
    @@ -89,16 +103,69 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
                             }
                     PopupAlertManager.postVectorAlert(alert)
                 }
    -            SasVerificationTxState.Cancelled,
    -            SasVerificationTxState.OnCancelled,
    -            SasVerificationTxState.Verified  -> {
    +            is VerificationTxState.TerminalTxState -> {
                     // cancel related notification
    -                PopupAlertManager.cancelAlert("kvr_${tx.transactionId}")
    +                PopupAlertManager.cancelAlert(uid)
                 }
    -            else                             -> Unit
    +            else                                   -> Unit
             }
         }
     
         override fun markedAsManuallyVerified(userId: String, deviceId: String) {
         }
    +
    +    override fun verificationRequestCreated(pr: PendingVerificationRequest) {
    +        // For incoming request we should prompt (if not in activity where this request apply)
    +        if (pr.isIncoming) {
    +            val name = session?.getUser(pr.otherUserId)?.displayName
    +                    ?: pr.otherUserId
    +
    +            val alert = PopupAlertManager.VectorAlert(
    +                    uniqueIdForVerificationRequest(pr),
    +                    context.getString(R.string.sas_incoming_request_notif_title),
    +                    "$name(${pr.otherUserId})",
    +                    R.drawable.ic_shield_black,
    +                    shouldBeDisplayedIn = { activity ->
    +                        if (activity is RoomDetailActivity) {
    +                            activity.intent?.extras?.getParcelable(RoomDetailActivity.EXTRA_ROOM_DETAIL_ARGS)?.let {
    +                                it.roomId != pr.roomId
    +                            } ?: true
    +                        } else true
    +                    })
    +                    .apply {
    +                        contentAction = Runnable {
    +                            (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                val roomId = pr.roomId
    +                                if (roomId.isNullOrBlank()) {
    +                                    it.navigator.waitSessionVerification(it)
    +                                } else {
    +                                    it.navigator.openRoom(it, roomId, pr.transactionId)
    +                                }
    +                            }
    +                        }
    +                        dismissedAction = Runnable {
    +                            session?.getVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
    +                                    pr.requestInfo?.fromDevice ?: "",
    +                                    pr.transactionId ?: "",
    +                                    pr.roomId ?: ""
    +                            )
    +                        }
    +                        colorInt = ThemeUtils.getColor(context, R.attr.vctr_notice_secondary)
    +                        // 5mn expiration
    +                        expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L)
    +                    }
    +            PopupAlertManager.postVectorAlert(alert)
    +        }
    +    }
    +
    +    override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
    +        // If an incoming request is readied (by another device?) we should discard the alert
    +        if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) {
    +            PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))
    +        }
    +        super.verificationRequestUpdated(pr)
    +    }
    +
    +    private fun uniqueIdForVerificationRequest(pr: PendingVerificationRequest) =
    +            "verificationRequest_${pr.transactionId}"
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt
    deleted file mode 100644
    index cf80bf98fc..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt
    +++ /dev/null
    @@ -1,244 +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
    -
    -import android.app.Activity
    -import android.content.Context
    -import android.content.Intent
    -import android.view.MenuItem
    -import androidx.appcompat.app.AlertDialog
    -import androidx.lifecycle.Observer
    -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.OutgoingSasVerificationRequest
    -import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
    -import im.vector.riotx.R
    -import im.vector.riotx.core.extensions.commitTransaction
    -import im.vector.riotx.core.extensions.observeEvent
    -import im.vector.riotx.core.platform.SimpleFragmentActivity
    -import im.vector.riotx.core.platform.WaitingViewData
    -
    -class SASVerificationActivity : SimpleFragmentActivity() {
    -
    -    companion object {
    -
    -        private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
    -        private const val EXTRA_TRANSACTION_ID = "EXTRA_TRANSACTION_ID"
    -        private const val EXTRA_OTHER_USER_ID = "EXTRA_OTHER_USER_ID"
    -        private const val EXTRA_OTHER_DEVICE_ID = "EXTRA_OTHER_DEVICE_ID"
    -        private const val EXTRA_IS_INCOMING = "EXTRA_IS_INCOMING"
    -
    -        /* ==========================================================================================
    -         * INPUT
    -         * ========================================================================================== */
    -
    -        fun incomingIntent(context: Context, matrixID: String, otherUserId: String, transactionID: String): Intent {
    -            val intent = Intent(context, SASVerificationActivity::class.java)
    -            intent.putExtra(EXTRA_MATRIX_ID, matrixID)
    -            intent.putExtra(EXTRA_TRANSACTION_ID, transactionID)
    -            intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId)
    -            intent.putExtra(EXTRA_IS_INCOMING, true)
    -            return intent
    -        }
    -
    -        fun outgoingIntent(context: Context, matrixID: String, otherUserId: String, otherDeviceId: String): Intent {
    -            val intent = Intent(context, SASVerificationActivity::class.java)
    -            intent.putExtra(EXTRA_MATRIX_ID, matrixID)
    -            intent.putExtra(EXTRA_OTHER_DEVICE_ID, otherDeviceId)
    -            intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId)
    -            intent.putExtra(EXTRA_IS_INCOMING, false)
    -            return intent
    -        }
    -
    -        /* ==========================================================================================
    -         * OUTPUT
    -         * ========================================================================================== */
    -
    -        fun getOtherUserId(intent: Intent?): String? {
    -            return intent?.getStringExtra(EXTRA_OTHER_USER_ID)
    -        }
    -
    -        fun getOtherDeviceId(intent: Intent?): String? {
    -            return intent?.getStringExtra(EXTRA_OTHER_DEVICE_ID)
    -        }
    -    }
    -
    -    override fun getTitleRes() = R.string.title_activity_verify_device
    -
    -    private lateinit var viewModel: SasVerificationViewModel
    -
    -    override fun initUiAndData() {
    -        super.initUiAndData()
    -        viewModel = viewModelProvider.get(SasVerificationViewModel::class.java)
    -        val transactionID: String? = intent.getStringExtra(EXTRA_TRANSACTION_ID)
    -
    -        if (isFirstCreation()) {
    -            val isIncoming = intent.getBooleanExtra(EXTRA_IS_INCOMING, false)
    -            if (isIncoming) {
    -                // incoming always have a transaction id
    -                viewModel.initIncoming(session, intent.getStringExtra(EXTRA_OTHER_USER_ID), transactionID)
    -            } else {
    -                viewModel.initOutgoing(session, intent.getStringExtra(EXTRA_OTHER_USER_ID), intent.getStringExtra(EXTRA_OTHER_DEVICE_ID))
    -            }
    -
    -            if (isIncoming) {
    -                val incoming = viewModel.transaction as? IncomingSasVerificationTransaction
    -                when (incoming?.uxState) {
    -                    null,
    -                    IncomingSasVerificationTransaction.UxState.UNKNOWN,
    -                    IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT,
    -                    IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> {
    -                        supportActionBar?.setTitle(R.string.sas_incoming_request_title)
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationIncomingFragment::class.java, null)
    -                        }
    -                    }
    -                    IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION,
    -                    IncomingSasVerificationTransaction.UxState.SHOW_SAS               -> {
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationShortCodeFragment::class.java, null)
    -                        }
    -                    }
    -                    IncomingSasVerificationTransaction.UxState.VERIFIED               -> {
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationVerifiedFragment::class.java, null)
    -                        }
    -                    }
    -                    IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
    -                    IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER     -> {
    -                        viewModel.navigateCancel()
    -                    }
    -                }
    -            } else {
    -                val outgoing = viewModel.transaction as? OutgoingSasVerificationRequest
    -                // transaction can be null, as not yet created
    -                when (outgoing?.uxState) {
    -                    null,
    -                    OutgoingSasVerificationRequest.UxState.UNKNOWN,
    -                    OutgoingSasVerificationRequest.UxState.WAIT_FOR_START,
    -                    OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> {
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationStartFragment::class.java, null)
    -                        }
    -                    }
    -                    OutgoingSasVerificationRequest.UxState.SHOW_SAS,
    -                    OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION  -> {
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationShortCodeFragment::class.java, null)
    -                        }
    -                    }
    -                    OutgoingSasVerificationRequest.UxState.VERIFIED               -> {
    -                        supportFragmentManager.commitTransaction {
    -                            setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out)
    -                            replace(R.id.container, SASVerificationVerifiedFragment::class.java, null)
    -                        }
    -                    }
    -                    OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
    -                    OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER     -> {
    -                        viewModel.navigateCancel()
    -                    }
    -                }
    -            }
    -        }
    -
    -        viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
    -            when (uxStateEvent) {
    -                SasVerificationViewModel.NAVIGATE_FINISH         -> {
    -                    finish()
    -                }
    -                SasVerificationViewModel.NAVIGATE_FINISH_SUCCESS -> {
    -                    val dataResult = Intent()
    -                    dataResult.putExtra(EXTRA_OTHER_DEVICE_ID, viewModel.otherDeviceId)
    -                    dataResult.putExtra(EXTRA_OTHER_USER_ID, viewModel.otherUserId)
    -                    setResult(Activity.RESULT_OK, dataResult)
    -                    finish()
    -                }
    -                SasVerificationViewModel.NAVIGATE_SAS_DISPLAY    -> {
    -                    supportFragmentManager.commitTransaction {
    -                        setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out)
    -                        replace(R.id.container, SASVerificationShortCodeFragment::class.java, null)
    -                    }
    -                }
    -                SasVerificationViewModel.NAVIGATE_SUCCESS        -> {
    -                    supportFragmentManager.commitTransaction {
    -                        setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out)
    -                        replace(R.id.container, SASVerificationVerifiedFragment::class.java, null)
    -                    }
    -                }
    -                SasVerificationViewModel.NAVIGATE_CANCELLED      -> {
    -                    val isCancelledByMe = viewModel.transaction?.state == SasVerificationTxState.Cancelled
    -                    val humanReadableReason = when (viewModel.transaction?.cancelledReason) {
    -                        CancelCode.User                 -> getString(R.string.sas_error_m_user)
    -                        CancelCode.Timeout              -> getString(R.string.sas_error_m_timeout)
    -                        CancelCode.UnknownTransaction   -> getString(R.string.sas_error_m_unknown_transaction)
    -                        CancelCode.UnknownMethod        -> getString(R.string.sas_error_m_unknown_method)
    -                        CancelCode.MismatchedCommitment -> getString(R.string.sas_error_m_mismatched_commitment)
    -                        CancelCode.MismatchedSas        -> getString(R.string.sas_error_m_mismatched_sas)
    -                        CancelCode.UnexpectedMessage    -> getString(R.string.sas_error_m_unexpected_message)
    -                        CancelCode.InvalidMessage       -> getString(R.string.sas_error_m_invalid_message)
    -                        CancelCode.MismatchedKeys       -> getString(R.string.sas_error_m_key_mismatch)
    -                        // Use user error
    -                        CancelCode.UserMismatchError    -> getString(R.string.sas_error_m_user_error)
    -                        null                            -> getString(R.string.sas_error_unknown)
    -                    }
    -                    val message =
    -                            if (isCancelledByMe) getString(R.string.sas_cancelled_by_me, humanReadableReason)
    -                            else getString(R.string.sas_cancelled_by_other, humanReadableReason)
    -                    // Show a dialog
    -                    if (!this.isFinishing) {
    -                        AlertDialog.Builder(this)
    -                                .setTitle(R.string.sas_cancelled_dialog_title)
    -                                .setMessage(message)
    -                                .setCancelable(false)
    -                                .setPositiveButton(R.string.ok) { _, _ ->
    -                                    // nop
    -                                    finish()
    -                                }
    -                                .show()
    -                    }
    -                }
    -            }
    -        }
    -
    -        viewModel.loadingLiveEvent.observe(this, Observer {
    -            if (it == null) {
    -                hideWaitingView()
    -            } else {
    -                val status = if (it == -1) "" else getString(it)
    -                updateWaitingView(WaitingViewData(status, isIndeterminate = true))
    -            }
    -        })
    -    }
    -
    -    override fun onOptionsItemSelected(item: MenuItem): Boolean {
    -        if (item.itemId == android.R.id.home) {
    -            // we want to cancel the transaction
    -            viewModel.cancelTransaction()
    -        }
    -
    -        return super.onOptionsItemSelected(item)
    -    }
    -
    -    override fun onBackPressed() {
    -        // we want to cancel the transaction
    -        viewModel.cancelTransaction()
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt
    deleted file mode 100644
    index 61f5c5f9fe..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt
    +++ /dev/null
    @@ -1,99 +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
    -
    -import android.os.Bundle
    -import android.widget.ImageView
    -import android.widget.TextView
    -import androidx.lifecycle.Observer
    -import butterknife.BindView
    -import butterknife.OnClick
    -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
    -import im.vector.matrix.android.api.util.MatrixItem
    -import im.vector.matrix.android.api.util.toMatrixItem
    -import im.vector.riotx.R
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import im.vector.riotx.features.home.AvatarRenderer
    -import javax.inject.Inject
    -
    -class SASVerificationIncomingFragment @Inject constructor(
    -        private var avatarRenderer: AvatarRenderer
    -) : VectorBaseFragment() {
    -
    -    @BindView(R.id.sas_incoming_request_user_display_name)
    -    lateinit var otherUserDisplayNameTextView: TextView
    -
    -    @BindView(R.id.sas_incoming_request_user_id)
    -    lateinit var otherUserIdTextView: TextView
    -
    -    @BindView(R.id.sas_incoming_request_user_device)
    -    lateinit var otherDeviceTextView: TextView
    -
    -    @BindView(R.id.sas_incoming_request_user_avatar)
    -    lateinit var avatarImageView: ImageView
    -
    -    override fun getLayoutResId() = R.layout.fragment_sas_verification_incoming_request
    -
    -    private lateinit var viewModel: SasVerificationViewModel
    -
    -    override fun onActivityCreated(savedInstanceState: Bundle?) {
    -        super.onActivityCreated(savedInstanceState)
    -
    -        viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java)
    -
    -        otherUserDisplayNameTextView.text = viewModel.otherUser?.displayName ?: viewModel.otherUserId
    -        otherUserIdTextView.text = viewModel.otherUserId
    -        otherDeviceTextView.text = viewModel.otherDeviceId
    -
    -        viewModel.otherUser?.let {
    -            avatarRenderer.render(it.toMatrixItem(), avatarImageView)
    -        } ?: run {
    -            // Fallback to what we know
    -            avatarRenderer.render(MatrixItem.UserItem(viewModel.otherUserId ?: "", viewModel.otherUserId), avatarImageView)
    -        }
    -
    -        viewModel.transactionState.observe(viewLifecycleOwner, Observer {
    -            val uxState = (viewModel.transaction as? IncomingSasVerificationTransaction)?.uxState
    -            when (uxState) {
    -                IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT            -> {
    -                    viewModel.loadingLiveEvent.value = null
    -                }
    -                IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> {
    -                    viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
    -                }
    -                IncomingSasVerificationTransaction.UxState.SHOW_SAS               -> {
    -                    viewModel.shortCodeReady()
    -                }
    -                IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
    -                IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER     -> {
    -                    viewModel.loadingLiveEvent.value = null
    -                    viewModel.navigateCancel()
    -                }
    -                else                                                              -> Unit
    -            }
    -        })
    -    }
    -
    -    @OnClick(R.id.sas_request_continue_button)
    -    fun didAccept() {
    -        viewModel.acceptTransaction()
    -    }
    -
    -    @OnClick(R.id.sas_request_cancel_button)
    -    fun didCancel() {
    -        viewModel.cancelTransaction()
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt
    deleted file mode 100644
    index ec9a943449..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt
    +++ /dev/null
    @@ -1,169 +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
    -
    -import android.os.Bundle
    -import android.view.ViewGroup
    -import android.widget.TextView
    -import androidx.core.view.isInvisible
    -import androidx.core.view.isVisible
    -import androidx.lifecycle.Observer
    -import butterknife.BindView
    -import butterknife.OnClick
    -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
    -import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
    -import im.vector.riotx.R
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import javax.inject.Inject
    -
    -class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() {
    -
    -    private lateinit var viewModel: SasVerificationViewModel
    -
    -    @BindView(R.id.sas_decimal_code)
    -    lateinit var decimalTextView: TextView
    -
    -    @BindView(R.id.sas_emoji_description)
    -    lateinit var descriptionTextView: TextView
    -
    -    @BindView(R.id.sas_emoji_grid)
    -    lateinit var emojiGrid: ViewGroup
    -
    -    @BindView(R.id.emoji0)
    -    lateinit var emoji0View: ViewGroup
    -    @BindView(R.id.emoji1)
    -    lateinit var emoji1View: ViewGroup
    -    @BindView(R.id.emoji2)
    -    lateinit var emoji2View: ViewGroup
    -    @BindView(R.id.emoji3)
    -    lateinit var emoji3View: ViewGroup
    -    @BindView(R.id.emoji4)
    -    lateinit var emoji4View: ViewGroup
    -    @BindView(R.id.emoji5)
    -    lateinit var emoji5View: ViewGroup
    -    @BindView(R.id.emoji6)
    -    lateinit var emoji6View: ViewGroup
    -
    -    override fun getLayoutResId() = R.layout.fragment_sas_verification_display_code
    -
    -    override fun onActivityCreated(savedInstanceState: Bundle?) {
    -        super.onActivityCreated(savedInstanceState)
    -        viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java)
    -
    -        viewModel.transaction?.let {
    -            if (it.supportsEmoji()) {
    -                val emojicodes = it.getEmojiCodeRepresentation()
    -                emojicodes.forEachIndexed { index, emojiRepresentation ->
    -                    when (index) {
    -                        0 -> {
    -                            emoji0View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                        1 -> {
    -                            emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                        2 -> {
    -                            emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                        3 -> {
    -                            emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
    -                        }
    -                        4 -> {
    -                            emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                        5 -> {
    -                            emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                        6 -> {
    -                            emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji
    -                            emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
    -                        }
    -                    }
    -                }
    -            }
    -
    -            // decimal is at least supported
    -            decimalTextView.text = it.getDecimalCodeRepresentation()
    -
    -            if (it.supportsEmoji()) {
    -                descriptionTextView.text = getString(R.string.sas_emoji_description)
    -                decimalTextView.isVisible = false
    -                emojiGrid.isVisible = true
    -            } else {
    -                descriptionTextView.text = getString(R.string.sas_decimal_description)
    -                decimalTextView.isVisible = true
    -                emojiGrid.isInvisible = true
    -            }
    -        }
    -
    -        viewModel.transactionState.observe(viewLifecycleOwner, Observer {
    -            if (viewModel.transaction is IncomingSasVerificationTransaction) {
    -                val uxState = (viewModel.transaction as IncomingSasVerificationTransaction).uxState
    -                when (uxState) {
    -                    IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                    }
    -                    IncomingSasVerificationTransaction.UxState.VERIFIED -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                        viewModel.deviceIsVerified()
    -                    }
    -                    IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME,
    -                    IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                        viewModel.navigateCancel()
    -                    }
    -                    else -> {
    -                        viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
    -                    }
    -                }
    -            } else if (viewModel.transaction is OutgoingSasVerificationRequest) {
    -                val uxState = (viewModel.transaction as OutgoingSasVerificationRequest).uxState
    -                when (uxState) {
    -                    OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                    }
    -                    OutgoingSasVerificationRequest.UxState.VERIFIED -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                        viewModel.deviceIsVerified()
    -                    }
    -                    OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
    -                    OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> {
    -                        viewModel.loadingLiveEvent.value = null
    -                        viewModel.navigateCancel()
    -                    }
    -                    else -> {
    -                        viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner
    -                    }
    -                }
    -            }
    -        })
    -    }
    -
    -    @OnClick(R.id.sas_request_continue_button)
    -    fun didAccept() {
    -        viewModel.confirmEmojiSame()
    -    }
    -
    -    @OnClick(R.id.sas_request_cancel_button)
    -    fun didCancel() {
    -        viewModel.cancelTransaction()
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt
    deleted file mode 100644
    index d9c3b1d155..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationStartFragment.kt
    +++ /dev/null
    @@ -1,119 +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
    -
    -import android.os.Bundle
    -import android.view.ViewGroup
    -import android.widget.Button
    -import android.widget.ProgressBar
    -import android.widget.TextView
    -import androidx.core.view.isInvisible
    -import androidx.core.view.isVisible
    -import androidx.lifecycle.Observer
    -import androidx.transition.TransitionManager
    -import butterknife.BindView
    -import butterknife.OnClick
    -import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
    -import im.vector.riotx.R
    -import im.vector.riotx.core.platform.VectorBaseActivity
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import javax.inject.Inject
    -
    -class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
    -
    -    override fun getLayoutResId() = R.layout.fragment_sas_verification_start
    -
    -    private lateinit var viewModel: SasVerificationViewModel
    -
    -    @BindView(R.id.rootLayout)
    -    lateinit var rootLayout: ViewGroup
    -
    -    @BindView(R.id.sas_start_button)
    -    lateinit var startButton: Button
    -
    -    @BindView(R.id.sas_start_button_loading)
    -    lateinit var startButtonLoading: ProgressBar
    -
    -    @BindView(R.id.sas_verifying_keys)
    -    lateinit var loadingText: TextView
    -
    -    override fun onActivityCreated(savedInstanceState: Bundle?) {
    -        super.onActivityCreated(savedInstanceState)
    -        viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java)
    -        viewModel.transactionState.observe(viewLifecycleOwner, Observer {
    -            val uxState = (viewModel.transaction as? OutgoingSasVerificationRequest)?.uxState
    -            when (uxState) {
    -                OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> {
    -                    // display loading
    -                    TransitionManager.beginDelayedTransition(this.rootLayout)
    -                    this.loadingText.isVisible = true
    -                    this.startButton.isInvisible = true
    -                    this.startButtonLoading.isVisible = true
    -                    this.startButtonLoading.animate()
    -                }
    -                OutgoingSasVerificationRequest.UxState.SHOW_SAS               -> {
    -                    viewModel.shortCodeReady()
    -                }
    -                OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME,
    -                OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER     -> {
    -                    viewModel.navigateCancel()
    -                }
    -                else                                                          -> {
    -                    TransitionManager.beginDelayedTransition(this.rootLayout)
    -                    this.loadingText.isVisible = false
    -                    this.startButton.isVisible = true
    -                    this.startButtonLoading.isVisible = false
    -                }
    -            }
    -        })
    -    }
    -
    -    @OnClick(R.id.sas_start_button)
    -    fun doStart() {
    -        viewModel.beginSasKeyVerification()
    -    }
    -
    -    @OnClick(R.id.sas_legacy_verification)
    -    fun doLegacy() {
    -        (requireActivity() as VectorBaseActivity).notImplemented()
    -
    -        /*
    -        viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId
    -                ?: "", object : SimpleApiCallback() {
    -            override fun onSuccess(info: MXDeviceInfo?) {
    -                info?.let {
    -
    -                    CommonActivityUtils.displayDeviceVerificationDialogLegacy(it, it.userId, viewModel.session, activity, object : YesNoListener {
    -                        override fun yes() {
    -                            viewModel.manuallyVerified()
    -                        }
    -
    -                        override fun no() {
    -
    -                        }
    -                    })
    -                }
    -            }
    -        })
    -        */
    -    }
    -
    -    @OnClick(R.id.sas_cancel_button)
    -    fun doCancel() {
    -        // Transaction may be started, or not
    -        viewModel.cancelTransaction()
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt
    deleted file mode 100644
    index 17beb21aff..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt
    +++ /dev/null
    @@ -1,40 +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
    -
    -import android.os.Bundle
    -import butterknife.OnClick
    -import im.vector.riotx.R
    -import im.vector.riotx.core.platform.VectorBaseFragment
    -import javax.inject.Inject
    -
    -class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() {
    -
    -    override fun getLayoutResId() = R.layout.fragment_sas_verification_verified
    -
    -    private lateinit var viewModel: SasVerificationViewModel
    -
    -    override fun onActivityCreated(savedInstanceState: Bundle?) {
    -        super.onActivityCreated(savedInstanceState)
    -
    -        viewModel = activityViewModelProvider.get(SasVerificationViewModel::class.java)
    -    }
    -
    -    @OnClick(R.id.sas_verification_verified_done_button)
    -    fun onDone() {
    -        viewModel.finishSuccess()
    -    }
    -}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt
    deleted file mode 100644
    index f14a85c516..0000000000
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SasVerificationViewModel.kt
    +++ /dev/null
    @@ -1,152 +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
    -
    -import androidx.lifecycle.LiveData
    -import androidx.lifecycle.MutableLiveData
    -import androidx.lifecycle.ViewModel
    -import im.vector.matrix.android.api.session.Session
    -import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
    -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.user.model.User
    -import im.vector.riotx.core.utils.LiveEvent
    -import javax.inject.Inject
    -
    -class SasVerificationViewModel @Inject constructor() : ViewModel(),
    -        SasVerificationService.SasVerificationListener {
    -
    -    companion object {
    -        const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
    -        const val NAVIGATE_FINISH_SUCCESS = "NAVIGATE_FINISH_SUCCESS"
    -        const val NAVIGATE_SAS_DISPLAY = "NAVIGATE_SAS_DISPLAY"
    -        const val NAVIGATE_SUCCESS = "NAVIGATE_SUCCESS"
    -        const val NAVIGATE_CANCELLED = "NAVIGATE_CANCELLED"
    -    }
    -
    -    private lateinit var sasVerificationService: SasVerificationService
    -
    -    var otherUserId: String? = null
    -    var otherDeviceId: String? = null
    -    var otherUser: User? = null
    -    var transaction: SasVerificationTransaction? = null
    -
    -    var transactionState: MutableLiveData = MutableLiveData()
    -
    -    init {
    -        // Force a first observe
    -        transactionState.value = null
    -    }
    -
    -    private var _navigateEvent: MutableLiveData> = MutableLiveData()
    -    val navigateEvent: LiveData>
    -        get() = _navigateEvent
    -
    -    var loadingLiveEvent: MutableLiveData = MutableLiveData()
    -
    -    var transactionID: String? = null
    -        set(value) {
    -            if (value != null) {
    -                transaction = sasVerificationService.getExistingTransaction(otherUserId!!, value)
    -                transactionState.value = transaction?.state
    -                otherDeviceId = transaction?.otherDeviceId
    -            }
    -            field = value
    -        }
    -
    -    fun initIncoming(session: Session, otherUserId: String, transactionID: String?) {
    -        this.sasVerificationService = session.getSasVerificationService()
    -        this.otherUserId = otherUserId
    -        this.transactionID = transactionID
    -        this.sasVerificationService.addListener(this)
    -        this.otherUser = session.getUser(otherUserId)
    -        if (transactionID == null || transaction == null) {
    -            // sanity, this transaction is not known anymore
    -            _navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
    -        }
    -    }
    -
    -    fun initOutgoing(session: Session, otherUserId: String, otherDeviceId: String) {
    -        this.sasVerificationService = session.getSasVerificationService()
    -        this.otherUserId = otherUserId
    -        this.otherDeviceId = otherDeviceId
    -        this.sasVerificationService.addListener(this)
    -        this.otherUser = session.getUser(otherUserId)
    -    }
    -
    -    fun beginSasKeyVerification() {
    -        val verificationSAS = sasVerificationService.beginKeyVerificationSAS(otherUserId!!, otherDeviceId!!)
    -        this.transactionID = verificationSAS
    -    }
    -
    -    override fun transactionCreated(tx: SasVerificationTransaction) {
    -    }
    -
    -    override fun transactionUpdated(tx: SasVerificationTransaction) {
    -        if (transactionID == tx.transactionId) {
    -            transactionState.value = tx.state
    -        }
    -    }
    -
    -    override fun markedAsManuallyVerified(userId: String, deviceId: String) {
    -    }
    -
    -    fun cancelTransaction() {
    -        transaction?.cancel()
    -        _navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
    -    }
    -
    -    fun finishSuccess() {
    -        _navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS)
    -    }
    -
    -    fun manuallyVerified() {
    -        if (otherUserId != null && otherDeviceId != null) {
    -            sasVerificationService.markedLocallyAsManuallyVerified(otherUserId!!, otherDeviceId!!)
    -        }
    -        _navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS)
    -    }
    -
    -    fun acceptTransaction() {
    -        (transaction as? IncomingSasVerificationTransaction)?.performAccept()
    -    }
    -
    -    fun confirmEmojiSame() {
    -        transaction?.userHasVerifiedShortCode()
    -    }
    -
    -    fun shortCodeReady() {
    -        loadingLiveEvent.value = null
    -        _navigateEvent.value = LiveEvent(NAVIGATE_SAS_DISPLAY)
    -    }
    -
    -    fun deviceIsVerified() {
    -        loadingLiveEvent.value = null
    -        _navigateEvent.value = LiveEvent(NAVIGATE_SUCCESS)
    -    }
    -
    -    fun navigateCancel() {
    -        _navigateEvent.value = LiveEvent(NAVIGATE_CANCELLED)
    -    }
    -
    -    override fun onCleared() {
    -        super.onCleared()
    -        if (::sasVerificationService.isInitialized) {
    -            sasVerificationService.removeListener(this)
    -        }
    -    }
    -}
    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..74c85a75c6
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    @@ -0,0 +1,31 @@
    +/*
    + * 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
    +
    +// TODO Remove otherUserId and transactionId when it's not necessary. Should be known by the ViewModel, no?
    +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()
    +    object OtherUserScannedSuccessfully : VerificationAction()
    +    object OtherUserDidNotScanned : 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
    new file mode 100644
    index 0000000000..b4f9892252
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    @@ -0,0 +1,264 @@
    +/*
    + * 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
    +
    +import android.os.Bundle
    +import android.os.Parcelable
    +import android.view.View
    +import android.widget.ImageView
    +import android.widget.TextView
    +import androidx.coordinatorlayout.widget.CoordinatorLayout
    +import androidx.core.view.isVisible
    +import androidx.fragment.app.Fragment
    +import androidx.lifecycle.Observer
    +import androidx.transition.AutoTransition
    +import androidx.transition.TransitionManager
    +import butterknife.BindView
    +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.VerificationTxState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.extensions.commitTransactionNow
    +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
    +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.qrconfirmation.VerificationQrScannedByOtherFragment
    +import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment
    +import im.vector.riotx.features.home.AvatarRenderer
    +import kotlinx.android.parcel.Parcelize
    +import kotlinx.android.synthetic.main.bottom_sheet_verification.*
    +import timber.log.Timber
    +import javax.inject.Inject
    +import kotlin.reflect.KClass
    +
    +class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +
    +    @Parcelize
    +    data class VerificationArgs(
    +            val otherUserId: String,
    +            val verificationId: String? = null,
    +            val roomId: String? = null,
    +            // Special mode where UX should show loading wheel until other user sends a request/tx
    +            val waitForIncomingRequest : Boolean = false
    +    ) : Parcelable
    +
    +    @Inject
    +    lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
    +    @Inject
    +    lateinit var avatarRenderer: AvatarRenderer
    +
    +    private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class)
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    @BindView(R.id.verificationRequestName)
    +    lateinit var otherUserNameText: TextView
    +
    +    @BindView(R.id.verificationRequestShield)
    +    lateinit var otherUserShield: View
    +
    +    @BindView(R.id.verificationRequestAvatar)
    +    lateinit var otherUserAvatarImageView: ImageView
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_verification
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +
    +        viewModel.requestLiveData.observe(viewLifecycleOwner, Observer {
    +            it.peekContent().let { va ->
    +                when (va) {
    +                    is Success -> {
    +                        if (va.invoke() is VerificationAction.GotItConclusion) {
    +                            dismiss()
    +                        }
    +                    }
    +                }
    +            }
    +        })
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        state.otherUserMxItem?.let { matrixItem ->
    +            if (state.isMe) {
    +                if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) {
    +                    otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_trusted)
    +                } else {
    +                    otherUserAvatarImageView.setImageResource(R.drawable.ic_shield_warning)
    +                }
    +                otherUserNameText.text = getString(R.string.complete_security)
    +                otherUserShield.isVisible = false
    +            } else {
    +                avatarRenderer.render(matrixItem, otherUserAvatarImageView)
    +
    +                if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) {
    +                    otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName())
    +                    otherUserShield.isVisible = true
    +                } else {
    +                    otherUserNameText.text = getString(R.string.verification_verify_user, matrixItem.getBestName())
    +                    otherUserShield.isVisible = false
    +                }
    +            }
    +        }
    +
    +        // Did the request result in a SAS transaction?
    +        if (state.sasTransactionState != null) {
    +            when (state.sasTransactionState) {
    +                is VerificationTxState.None,
    +                is VerificationTxState.SendingStart,
    +                is VerificationTxState.Started,
    +                is VerificationTxState.OnStarted,
    +                is VerificationTxState.SendingAccept,
    +                is VerificationTxState.Accepted,
    +                is VerificationTxState.OnAccepted,
    +                is VerificationTxState.SendingKey,
    +                is VerificationTxState.KeySent,
    +                is VerificationTxState.OnKeyReceived,
    +                is VerificationTxState.ShortCodeReady,
    +                is VerificationTxState.ShortCodeAccepted,
    +                is VerificationTxState.SendingMac,
    +                is VerificationTxState.MacSent,
    +                is VerificationTxState.Verifying -> {
    +                    showFragment(VerificationEmojiCodeFragment::class, Bundle().apply {
    +                        putParcelable(MvRx.KEY_ARG, VerificationArgs(
    +                                state.otherUserMxItem?.id ?: "",
    +                                // If it was outgoing it.transaction id would be null, but the pending request
    +                                // would be updated (from localID to txId)
    +                                state.pendingRequest.invoke()?.transactionId ?: state.transactionId))
    +                    })
    +                }
    +                is VerificationTxState.Verified  -> {
    +                    showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                        putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
    +                    })
    +                }
    +                is VerificationTxState.Cancelled -> {
    +                    showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                        putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.sasTransactionState.cancelCode.value, state.isMe))
    +                    })
    +                }
    +            }
    +
    +            return@withState
    +        }
    +
    +        when (state.qrTransactionState) {
    +            is VerificationTxState.QrScannedByOther -> {
    +                showFragment(VerificationQrScannedByOtherFragment::class, Bundle())
    +                return@withState
    +            }
    +            is VerificationTxState.Verified         -> {
    +                showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
    +                })
    +                return@withState
    +            }
    +            is VerificationTxState.Cancelled        -> {
    +                showFragment(VerificationConclusionFragment::class, Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(false, state.qrTransactionState.cancelCode.value, state.isMe))
    +                })
    +                return@withState
    +            }
    +            else                                    -> Unit
    +        }
    +
    +        // At this point there is no SAS transaction for this request
    +
    +        // Transaction has not yet started
    +        if (state.pendingRequest.invoke()?.cancelConclusion != null) {
    +            // The request has been declined, we should dismiss
    +            dismiss()
    +        }
    +
    +        // If it's an outgoing
    +        if (state.pendingRequest.invoke() == null || state.pendingRequest.invoke()?.isIncoming == false || state.waitForOtherUserMode) {
    +            Timber.v("## SAS show bottom sheet for outgoing request")
    +            if (state.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(state.otherUserMxItem?.id
    +                            ?: "", state.pendingRequest.invoke()?.transactionId))
    +                })
    +            } else {
    +                // Stay on the start fragment
    +                showFragment(VerificationRequestFragment::class, Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationArgs(
    +                            state.otherUserMxItem?.id ?: "",
    +                            state.pendingRequest.invoke()?.transactionId,
    +                            state.roomId))
    +                })
    +            }
    +        } else if (state.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(state.otherUserMxItem?.id
    +                        ?: "", state.pendingRequest.invoke()?.transactionId))
    +            })
    +        }
    +        super.invalidate()
    +    }
    +
    +    private fun showFragment(fragmentClass: KClass, bundle: Bundle) {
    +        if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
    +            // We want to animate the bottomsheet bound changes
    +            bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
    +                TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
    +            }
    +            // Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
    +            childFragmentManager.commitTransactionNow {
    +                replace(R.id.bottomSheetFragmentContainer,
    +                        fragmentClass.java,
    +                        bundle,
    +                        fragmentClass.simpleName
    +                )
    +            }
    +        }
    +    }
    +
    +    companion object {
    +        fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null, waitForIncomingRequest: Boolean = false): VerificationBottomSheet {
    +            return VerificationBottomSheet().apply {
    +                arguments = Bundle().apply {
    +                    putParcelable(MvRx.KEY_ARG, VerificationArgs(
    +                            otherUserId = otherUserId,
    +                            roomId = roomId,
    +                            verificationId = transactionId,
    +                            waitForIncomingRequest = waitForIncomingRequest
    +                    ))
    +                }
    +            }
    +        }
    +
    +        val WAITING_SELF_VERIF_TAG : String = "WAITING_SELF_VERIF_TAG"
    +    }
    +}
    +
    +fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
    +    var current = this as? View
    +    while (current != null) {
    +        if (current is CoordinatorLayout) return current
    +        current = current.parent as? View
    +    }
    +    return null
    +}
    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
    new file mode 100644
    index 0000000000..5e11570ced
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -0,0 +1,349 @@
    +/*
    + * 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
    +
    +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.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.IncomingSasVerificationTransaction
    +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.extensions.exhaustive
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.core.utils.LiveEvent
    +
    +data class VerificationBottomSheetViewState(
    +        val otherUserMxItem: MatrixItem? = null,
    +        val roomId: String? = null,
    +        val pendingRequest: Async = Uninitialized,
    +        val pendingLocalId: String? = null,
    +        val sasTransactionState: VerificationTxState? = null,
    +        val qrTransactionState: VerificationTxState? = null,
    +        val transactionId: String? = null,
    +        // true when we display the loading and we wait for the other (incoming request)
    +        val waitForOtherUserMode: Boolean = false,
    +        val isMe: Boolean = false
    +) : MvRxState
    +
    +class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
    +                                                                   @Assisted args: VerificationBottomSheet.VerificationArgs,
    +                                                                   private val session: Session)
    +    : VectorViewModel(initialState),
    +        VerificationService.VerificationListener {
    +
    +    // Can be used for several actions, for a one shot result
    +    private val _requestLiveData = MutableLiveData>>()
    +    val requestLiveData: LiveData>>
    +        get() = _requestLiveData
    +
    +    init {
    +        session.getVerificationService().addListener(this)
    +
    +        val userItem = session.getUser(args.otherUserId)
    +
    +        val isWaitingForOtherMode = args.waitForIncomingRequest
    +
    +        var autoReady = false
    +        val pr = if (isWaitingForOtherMode) {
    +            // See if active tx for this user and take it
    +
    +            session.getVerificationService().getExistingVerificationRequest(args.otherUserId)
    +                    ?.lastOrNull { !it.isFinished }
    +                    ?.also { verificationRequest ->
    +                        if (verificationRequest.isIncoming && !verificationRequest.isReady) {
    +                            // auto ready in this case, as we are waiting
    +                            autoReady = true
    +                        }
    +                    }
    +        } else {
    +            session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    +        }
    +
    +        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
    +        }
    +
    +        setState {
    +            copy(
    +                    otherUserMxItem = userItem?.toMatrixItem(),
    +                    sasTransactionState = sasTx?.state,
    +                    qrTransactionState = qrTx?.state,
    +                    transactionId = pr?.transactionId ?: args.verificationId,
    +                    pendingRequest = if (pr != null) Success(pr) else Uninitialized,
    +                    waitForOtherUserMode = isWaitingForOtherMode,
    +                    roomId = args.roomId,
    +                    isMe = args.otherUserId == session.myUserId
    +            )
    +        }
    +
    +        if (autoReady) {
    +            // TODO, can I be here in DM mode? in this case should test if roomID is null?
    +            session.getVerificationService()
    +                    .readyPendingVerification(supportedVerificationMethods,
    +                            pr!!.otherUserId,
    +                            pr.transactionId ?: "")
    +        }
    +    }
    +
    +    override fun onCleared() {
    +        session.getVerificationService().removeListener(this)
    +        super.onCleared()
    +    }
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: VerificationBottomSheetViewState,
    +                   args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
    +            val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
    +            val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
    +
    +            return fragment.verificationViewModelFactory.create(state, args)
    +        }
    +    }
    +
    +    override fun handle(action: VerificationAction) = withState { state ->
    +        val otherUserId = state.otherUserMxItem?.id ?: return@withState
    +        val roomId = state.roomId
    +                ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
    +
    +        when (action) {
    +            is VerificationAction.RequestVerificationByDM      -> {
    +                if (roomId == null) {
    +                    val localID = LocalEcho.createLocalEchoId()
    +                    setState {
    +                        copy(
    +                                pendingLocalId = localID,
    +                                pendingRequest = Loading()
    +                        )
    +                    }
    +                    val roomParams = CreateRoomParams(
    +                            invitedUserIds = listOf(otherUserId)
    +                    )
    +                            .setDirectMessage()
    +                            .enableEncryptionIfInvitedUsersSupportIt()
    +
    +                    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)
    +                                )
    +                        )
    +                    }
    +                }
    +                Unit
    +            }
    +            is VerificationAction.StartSASVerification         -> {
    +                val request = session.getVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
    +                        ?: return@withState
    +                val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
    +                if (roomId == null) {
    +                    session.getVerificationService().beginKeyVerification(
    +                            VerificationMethod.SAS,
    +                            otherUserId = request.otherUserId,
    +                            otherDeviceId = otherDevice ?: "",
    +                            transactionId = action.pendingRequestTransactionId
    +                    )
    +                } else {
    +                    session.getVerificationService().beginKeyVerificationInDMs(
    +                            VerificationMethod.SAS,
    +                            transactionId = action.pendingRequestTransactionId,
    +                            roomId = roomId,
    +                            otherUserId = request.otherUserId,
    +                            otherDeviceId = otherDevice ?: "",
    +                            callback = null
    +                    )
    +                }
    +                Unit
    +            }
    +            is VerificationAction.RemoteQrCodeScanned          -> {
    +                val existingTransaction = session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
    +                existingTransaction
    +                        ?.userHasScannedOtherQrCode(action.scannedData)
    +            }
    +            is VerificationAction.OtherUserScannedSuccessfully -> {
    +                val transactionId = state.transactionId ?: return@withState
    +
    +                val existingTransaction = session.getVerificationService()
    +                        .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction
    +                existingTransaction
    +                        ?.otherUserScannedMyQrCode()
    +            }
    +            is VerificationAction.OtherUserDidNotScanned       -> {
    +                val transactionId = state.transactionId ?: return@withState
    +
    +                val existingTransaction = session.getVerificationService()
    +                        .getExistingTransaction(otherUserId, transactionId) as? QrCodeVerificationTransaction
    +                existingTransaction
    +                        ?.otherUserDidNotScannedMyQrCode()
    +            }
    +            is VerificationAction.SASMatchAction               -> {
    +                (session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.sasTransactionId)
    +                        as? SasVerificationTransaction)?.userHasVerifiedShortCode()
    +            }
    +            is VerificationAction.SASDoNotMatchAction          -> {
    +                (session.getVerificationService()
    +                        .getExistingTransaction(action.otherUserId, action.sasTransactionId)
    +                        as? SasVerificationTransaction)
    +                        ?.shortCodeDoesNotMatch()
    +            }
    +            is VerificationAction.GotItConclusion              -> {
    +                _requestLiveData.postValue(LiveEvent(Success(action)))
    +            }
    +        }.exhaustive
    +    }
    +
    +    override fun transactionCreated(tx: VerificationTransaction) {
    +        transactionUpdated(tx)
    +    }
    +
    +    override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    +        if (state.waitForOtherUserMode && state.transactionId == null) {
    +            // is this an incoming with that user
    +            if (tx.isIncoming && tx.otherUserId == state.otherUserMxItem?.id) {
    +                // Also auto accept incoming if needed!
    +                if (tx is IncomingSasVerificationTransaction) {
    +                    if (tx.uxState == IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) {
    +                        tx.performAccept()
    +                    }
    +                }
    +                // Use this one!
    +                setState {
    +                    copy(
    +                            transactionId = tx.transactionId,
    +                            sasTransactionState = tx.state.takeIf { tx is SasVerificationTransaction },
    +                            qrTransactionState = tx.state.takeIf { tx is QrCodeVerificationTransaction }
    +                    )
    +                }
    +            }
    +        }
    +
    +        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
    +                        )
    +                    }
    +                }
    +            }
    +            is QrCodeVerificationTransaction -> {
    +                if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
    +                    // A QR tx has been started following this request
    +                    setState {
    +                        copy(
    +                                qrTransactionState = tx.state
    +                        )
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun verificationRequestCreated(pr: PendingVerificationRequest) {
    +        verificationRequestUpdated(pr)
    +    }
    +
    +    override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
    +
    +        if (state.waitForOtherUserMode && state.pendingRequest.invoke() == null && state.transactionId == null) {
    +            // is this an incoming with that user
    +            if (pr.isIncoming && pr.otherUserId == state.otherUserMxItem?.id) {
    +                if (!pr.isReady) {
    +                    // auto ready in this case, as we are waiting
    +                    // TODO, can I be here in DM mode? in this case should test if roomID is null?
    +                    session.getVerificationService()
    +                            .readyPendingVerification(supportedVerificationMethods,
    +                                    pr.otherUserId,
    +                                    pr.transactionId ?: "")
    +                }
    +
    +                // Use this one!
    +                setState {
    +                    copy(
    +                            transactionId = pr.transactionId,
    +                            pendingRequest = Success(pr)
    +                    )
    +                }
    +                return@withState
    +            }
    +        }
    +
    +        if (pr.localID == state.pendingLocalId
    +                || pr.localID == state.pendingRequest.invoke()?.localID
    +                || state.pendingRequest.invoke()?.transactionId == pr.transactionId) {
    +            setState {
    +                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
    new file mode 100644
    index 0000000000..87bb843291
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    @@ -0,0 +1,104 @@
    +/*
    + * 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.choose
    +
    +import com.airbnb.epoxy.EpoxyController
    +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.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    +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(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider
    +) : EpoxyController() {
    +
    +    var listener: Listener? = null
    +
    +    private var viewState: VerificationChooseMethodViewState? = null
    +
    +    fun update(viewState: VerificationChooseMethodViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val state = viewState ?: return
    +
    +        if (state.otherCanScanQrCode || state.otherCanShowQrCode) {
    +            bottomSheetVerificationNoticeItem {
    +                id("notice")
    +                notice(stringProvider.getString(R.string.verification_scan_notice))
    +            }
    +
    +            if (state.otherCanScanQrCode && !state.qrCodeText.isNullOrBlank()) {
    +                bottomSheetVerificationQrCodeItem {
    +                    id("qr")
    +                    data(state.qrCodeText)
    +                    animate(false)
    +                }
    +
    +                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() }
    +                }
    +
    +                dividerItem {
    +                    id("sep1")
    +                }
    +            }
    +
    +            bottomSheetVerificationActionItem {
    +                id("openEmoji")
    +                title(stringProvider.getString(R.string.verification_scan_emoji_title))
    +                titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                subTitle(stringProvider.getString(R.string.verification_scan_emoji_subtitle))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                listener { listener?.doVerifyBySas() }
    +            }
    +        } else if (state.SASModeAvailable) {
    +            bottomSheetVerificationActionItem {
    +                id("openEmoji")
    +                title(stringProvider.getString(R.string.verification_no_scan_emoji_title))
    +                titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                iconRes(R.drawable.ic_arrow_right)
    +                iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                listener { listener?.doVerifyBySas() }
    +            }
    +        }
    +    }
    +
    +    interface Listener {
    +        fun openCamera()
    +        fun doVerifyBySas()
    +    }
    +}
    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
    new file mode 100644
    index 0000000000..e0b7f97383
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    @@ -0,0 +1,122 @@
    +/*
    + * 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.choose
    +
    +import android.app.Activity
    +import android.content.Intent
    +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
    +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(
    +        val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory,
    +        val controller: VerificationChooseMethodController
    +) : VectorBaseFragment(), VerificationChooseMethodController.Listener {
    +
    +    private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class)
    +
    +    private val sharedViewModel 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()
    +    }
    +
    +    override fun onDestroyView() {
    +        bottomSheetVerificationRecyclerView.cleanup()
    +        controller.listener = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
    +        controller.listener = this
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        controller.update(state)
    +    }
    +
    +    override fun doVerifyBySas() = withState(sharedViewModel) { state ->
    +        sharedViewModel.handle(VerificationAction.StartSASVerification(
    +                state.otherUserMxItem?.id ?: "",
    +                state.pendingRequest.invoke()?.transactionId ?: ""))
    +    }
    +
    +    override fun openCamera() {
    +        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
    new file mode 100644
    index 0000000000..75c1b69058
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    @@ -0,0 +1,115 @@
    +/*
    + * 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.choose
    +
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +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.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
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
    +
    +data class VerificationChooseMethodViewState(
    +        val otherUserId: String = "",
    +        val transactionId: String = "",
    +        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), VerificationService.VerificationListener {
    +
    +    override fun transactionCreated(tx: VerificationTransaction) {
    +        transactionUpdated(tx)
    +    }
    +
    +    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.getVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
    +
    +        setState {
    +            copy(
    +                    otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
    +                    otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
    +                    SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
    +            )
    +        }
    +    }
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel
    +    }
    +
    +    init {
    +        session.getVerificationService().addListener(this)
    +    }
    +
    +    override fun onCleared() {
    +        super.onCleared()
    +        session.getVerificationService().removeListener(this)
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +        override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? {
    +            val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment()
    +            return fragment.verificationChooseMethodViewModelFactory.create(state)
    +        }
    +
    +        override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
    +            val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
    +            val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    +            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 ?: "",
    +                    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
    +            )
    +        }
    +    }
    +
    +    override fun handle(action: EmptyAction) {}
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt
    new file mode 100644
    index 0000000000..9719651bd4
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionController.kt
    @@ -0,0 +1,98 @@
    +/*
    + * 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.conclusion
    +
    +import com.airbnb.epoxy.EpoxyController
    +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.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.html.EventHtmlRenderer
    +import javax.inject.Inject
    +
    +class VerificationConclusionController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider,
    +        private val eventHtmlRenderer: EventHtmlRenderer
    +) : EpoxyController() {
    +
    +    var listener: Listener? = null
    +
    +    private var viewState: VerificationConclusionViewState? = null
    +
    +    fun update(viewState: VerificationConclusionViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val state = viewState ?: return
    +
    +        when (state.conclusionState) {
    +            ConclusionState.SUCCESS -> {
    +                bottomSheetVerificationNoticeItem {
    +                    id("notice")
    +                    notice(stringProvider.getString(
    +                            if (state.isSelfVerification) R.string.verification_conclusion_ok_self_notice
    +                            else R.string.verification_conclusion_ok_notice))
    +                }
    +
    +                bottomSheetVerificationBigImageItem {
    +                    id("image")
    +                    imageRes(R.drawable.ic_shield_trusted)
    +                }
    +            }
    +            ConclusionState.WARNING -> {
    +                bottomSheetVerificationNoticeItem {
    +                    id("notice")
    +                    notice(stringProvider.getString(R.string.verification_conclusion_not_secure))
    +                }
    +
    +                bottomSheetVerificationBigImageItem {
    +                    id("image")
    +                    imageRes(R.drawable.ic_shield_warning)
    +                }
    +
    +                bottomSheetVerificationNoticeItem {
    +                    id("warning_notice")
    +                    notice(eventHtmlRenderer.render(stringProvider.getString(R.string.verification_conclusion_compromised)))
    +                }
    +            }
    +            else                    -> Unit
    +        }
    +
    +        dividerItem {
    +            id("sep0")
    +        }
    +
    +        bottomSheetVerificationActionItem {
    +            id("done")
    +            title(stringProvider.getString(R.string.done))
    +            titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +            iconRes(R.drawable.ic_arrow_right)
    +            iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +            listener { listener?.onButtonTapped() }
    +        }
    +    }
    +
    +    interface Listener {
    +        fun onButtonTapped()
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt
    new file mode 100644
    index 0000000000..854809084e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionFragment.kt
    @@ -0,0 +1,80 @@
    +/*
    + * 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.conclusion
    +
    +import android.os.Bundle
    +import android.os.Parcelable
    +import android.view.View
    +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.extensions.cleanup
    +import im.vector.riotx.core.extensions.configureWith
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.features.crypto.verification.VerificationAction
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import kotlinx.android.parcel.Parcelize
    +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import javax.inject.Inject
    +
    +class VerificationConclusionFragment @Inject constructor(
    +        val controller: VerificationConclusionController
    +) : VectorBaseFragment(), VerificationConclusionController.Listener {
    +
    +    @Parcelize
    +    data class Args(
    +            val isSuccessFull: Boolean,
    +            val cancelReason: String?,
    +            val isMe: Boolean
    +    ) : Parcelable
    +
    +    private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
    +
    +    private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class)
    +
    +    override fun getLayoutResId() = R.layout.bottom_sheet_verification_child_fragment
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +
    +        setupRecyclerView()
    +    }
    +
    +    override fun onDestroyView() {
    +        bottomSheetVerificationRecyclerView.cleanup()
    +        controller.listener = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
    +        controller.listener = this
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        if (state.conclusionState == ConclusionState.CANCELLED) {
    +            // Just dismiss in this case
    +            sharedViewModel.handle(VerificationAction.GotItConclusion)
    +        } else {
    +            controller.update(state)
    +        }
    +    }
    +
    +    override fun onButtonTapped() {
    +        sharedViewModel.handle(VerificationAction.GotItConclusion)
    +    }
    +}
    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
    new file mode 100644
    index 0000000000..bdeb9e01dd
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/conclusion/VerificationConclusionViewModel.kt
    @@ -0,0 +1,65 @@
    +/*
    + * 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.conclusion
    +
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.ViewModelContext
    +import im.vector.matrix.android.api.session.crypto.sas.CancelCode
    +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
    +import im.vector.riotx.core.platform.EmptyAction
    +import im.vector.riotx.core.platform.EmptyViewEvents
    +import im.vector.riotx.core.platform.VectorViewModel
    +
    +data class VerificationConclusionViewState(
    +        val conclusionState: ConclusionState = ConclusionState.CANCELLED,
    +        val isSelfVerification: Boolean = false
    +) : MvRxState
    +
    +enum class ConclusionState {
    +    SUCCESS,
    +    WARNING,
    +    CANCELLED
    +}
    +
    +class VerificationConclusionViewModel(initialState: VerificationConclusionViewState)
    +    : VectorViewModel(initialState) {
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        override fun initialState(viewModelContext: ViewModelContext): VerificationConclusionViewState? {
    +            val args = viewModelContext.args()
    +
    +            return when (safeValueOf(args.cancelReason)) {
    +                CancelCode.QrCodeInvalid,
    +                CancelCode.MismatchedUser,
    +                CancelCode.MismatchedSas,
    +                CancelCode.MismatchedCommitment,
    +                CancelCode.MismatchedKeys -> {
    +                    VerificationConclusionViewState(ConclusionState.WARNING, args.isMe)
    +                }
    +                else                      -> {
    +                    VerificationConclusionViewState(
    +                            if (args.isSuccessFull) ConclusionState.SUCCESS else ConclusionState.CANCELLED,
    +                            args.isMe
    +                    )
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun handle(action: EmptyAction) {}
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeController.kt
    new file mode 100644
    index 0000000000..12fffd3e11
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeController.kt
    @@ -0,0 +1,163 @@
    +/*
    + * 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.emoji
    +
    +import com.airbnb.epoxy.EpoxyController
    +import com.airbnb.mvrx.Fail
    +import com.airbnb.mvrx.Success
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.dividerItem
    +import im.vector.riotx.core.epoxy.errorWithRetryItem
    +import im.vector.riotx.core.error.ErrorFormatter
    +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.bottomSheetVerificationDecimalCodeItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationEmojisItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
    +import javax.inject.Inject
    +
    +class VerificationEmojiCodeController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider,
    +        private val errorFormatter: ErrorFormatter
    +) : EpoxyController() {
    +
    +    var listener: Listener? = null
    +
    +    private var viewState: VerificationEmojiCodeViewState? = null
    +
    +    fun update(viewState: VerificationEmojiCodeViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val state = viewState ?: return
    +
    +        if (state.supportsEmoji) {
    +            buildEmojiItem(state)
    +        } else {
    +            buildDecimal(state)
    +        }
    +    }
    +
    +    private fun buildEmojiItem(state: VerificationEmojiCodeViewState) {
    +        when (val emojiDescription = state.emojiDescription) {
    +            is Success -> {
    +                bottomSheetVerificationNoticeItem {
    +                    id("notice")
    +                    notice(stringProvider.getString(R.string.verification_emoji_notice))
    +                }
    +
    +                bottomSheetVerificationEmojisItem {
    +                    id("emojis")
    +                    emojiRepresentation0(emojiDescription()[0])
    +                    emojiRepresentation1(emojiDescription()[1])
    +                    emojiRepresentation2(emojiDescription()[2])
    +                    emojiRepresentation3(emojiDescription()[3])
    +                    emojiRepresentation4(emojiDescription()[4])
    +                    emojiRepresentation5(emojiDescription()[5])
    +                    emojiRepresentation6(emojiDescription()[6])
    +                }
    +
    +                buildActions(state)
    +            }
    +            is Fail    -> {
    +                errorWithRetryItem {
    +                    id("error")
    +                    text(errorFormatter.toHumanReadable(emojiDescription.error))
    +                }
    +            }
    +            else       -> {
    +                bottomSheetVerificationWaitingItem {
    +                    id("waiting")
    +                    title(stringProvider.getString(R.string.please_wait))
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun buildDecimal(state: VerificationEmojiCodeViewState) {
    +        when (val decimalDescription = state.decimalDescription) {
    +            is Success -> {
    +                bottomSheetVerificationNoticeItem {
    +                    id("notice")
    +                    notice(stringProvider.getString(R.string.verification_code_notice))
    +                }
    +
    +                bottomSheetVerificationDecimalCodeItem {
    +                    id("decimal")
    +                    code(state.decimalDescription.invoke() ?: "")
    +                }
    +
    +                buildActions(state)
    +            }
    +            is Fail    -> {
    +                errorWithRetryItem {
    +                    id("error")
    +                    text(errorFormatter.toHumanReadable(decimalDescription.error))
    +                }
    +            }
    +            else       -> {
    +                bottomSheetVerificationWaitingItem {
    +                    id("waiting")
    +                    title(stringProvider.getString(R.string.please_wait))
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun buildActions(state: VerificationEmojiCodeViewState) {
    +        dividerItem {
    +            id("sep0")
    +        }
    +
    +        if (state.isWaitingFromOther) {
    +            bottomSheetVerificationWaitingItem {
    +                id("waiting")
    +                title(stringProvider.getString(R.string.verification_request_waiting_for, state.otherUser?.getBestName() ?: ""))
    +            }
    +        } else {
    +            bottomSheetVerificationActionItem {
    +                id("ko")
    +                title(stringProvider.getString(R.string.verification_sas_do_not_match))
    +                titleColor(colorProvider.getColor(R.color.vector_error_color))
    +                iconRes(R.drawable.ic_check_off)
    +                iconColor(colorProvider.getColor(R.color.vector_error_color))
    +                listener { listener?.onDoNotMatchButtonTapped() }
    +            }
    +            dividerItem {
    +                id("sep1")
    +            }
    +            bottomSheetVerificationActionItem {
    +                id("ok")
    +                title(stringProvider.getString(R.string.verification_sas_match))
    +                titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                iconRes(R.drawable.ic_check_on)
    +                iconColor(colorProvider.getColor(R.color.riotx_accent))
    +                listener { listener?.onMatchButtonTapped() }
    +            }
    +        }
    +    }
    +
    +    interface Listener {
    +        fun onDoNotMatchButtonTapped()
    +        fun onMatchButtonTapped()
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt
    new file mode 100644
    index 0000000000..2cd20ca4fb
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeFragment.kt
    @@ -0,0 +1,75 @@
    +/*
    + * 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.emoji
    +
    +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
    +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.features.crypto.verification.VerificationAction
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import javax.inject.Inject
    +
    +class VerificationEmojiCodeFragment @Inject constructor(
    +        val viewModelFactory: VerificationEmojiCodeViewModel.Factory,
    +        val controller: VerificationEmojiCodeController
    +) : VectorBaseFragment(), VerificationEmojiCodeController.Listener {
    +
    +    private val viewModel by fragmentViewModel(VerificationEmojiCodeViewModel::class)
    +
    +    private val sharedViewModel 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()
    +    }
    +
    +    override fun onDestroyView() {
    +        bottomSheetVerificationRecyclerView.cleanup()
    +        controller.listener = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
    +        controller.listener = this
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        controller.update(state)
    +    }
    +
    +    override fun onMatchButtonTapped() = withState(viewModel) { state ->
    +        val otherUserId = state.otherUser?.id ?: return@withState
    +        val txId = state.transactionId ?: return@withState
    +        sharedViewModel.handle(VerificationAction.SASMatchAction(otherUserId, txId))
    +    }
    +
    +    override fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
    +        val otherUserId = state.otherUser?.id ?: return@withState
    +        val txId = state.transactionId ?: return@withState
    +        sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(otherUserId, txId))
    +    }
    +}
    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
    new file mode 100644
    index 0000000000..69d106a4b4
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt
    @@ -0,0 +1,178 @@
    +/*
    + * 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.emoji
    +
    +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.session.Session
    +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
    +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
    +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.util.MatrixItem
    +import im.vector.matrix.android.api.util.toMatrixItem
    +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 VerificationEmojiCodeViewState(
    +        val transactionId: String?,
    +        val otherUser: MatrixItem? = null,
    +        val supportsEmoji: Boolean = true,
    +        val emojiDescription: Async> = Uninitialized,
    +        val decimalDescription: Async = Uninitialized,
    +        val isWaitingFromOther: Boolean = false
    +) : MvRxState
    +
    +class VerificationEmojiCodeViewModel @AssistedInject constructor(
    +        @Assisted initialState: VerificationEmojiCodeViewState,
    +        private val session: Session
    +) : VectorViewModel(initialState), VerificationService.VerificationListener {
    +
    +    init {
    +        withState { state ->
    +            refreshStateFromTx(session.getVerificationService()
    +                    .getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
    +                            ?: "") as? SasVerificationTransaction)
    +        }
    +
    +        session.getVerificationService().addListener(this)
    +    }
    +
    +    override fun onCleared() {
    +        session.getVerificationService().removeListener(this)
    +        super.onCleared()
    +    }
    +
    +    private fun refreshStateFromTx(sasTx: SasVerificationTransaction?) {
    +        when (sasTx?.state) {
    +            is VerificationTxState.None,
    +            is VerificationTxState.SendingStart,
    +            is VerificationTxState.Started,
    +            is VerificationTxState.OnStarted,
    +            is VerificationTxState.SendingAccept,
    +            is VerificationTxState.Accepted,
    +            is VerificationTxState.OnAccepted,
    +            is VerificationTxState.SendingKey,
    +            is VerificationTxState.KeySent,
    +            is VerificationTxState.OnKeyReceived  -> {
    +                setState {
    +                    copy(
    +                            isWaitingFromOther = false,
    +                            supportsEmoji = sasTx.supportsEmoji(),
    +                            emojiDescription = Loading>()
    +                                    .takeIf { sasTx.supportsEmoji() }
    +                                    ?: Uninitialized,
    +                            decimalDescription = Loading()
    +                                    .takeIf { sasTx.supportsEmoji().not() }
    +                                    ?: Uninitialized
    +                    )
    +                }
    +            }
    +            is VerificationTxState.ShortCodeReady -> {
    +                setState {
    +                    copy(
    +                            isWaitingFromOther = false,
    +                            supportsEmoji = sasTx.supportsEmoji(),
    +                            emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation())
    +                            else Uninitialized,
    +                            decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation())
    +                            else Uninitialized
    +                    )
    +                }
    +            }
    +            is VerificationTxState.ShortCodeAccepted,
    +            is VerificationTxState.SendingMac,
    +            is VerificationTxState.MacSent,
    +            is VerificationTxState.Verifying,
    +            is VerificationTxState.Verified       -> {
    +                setState {
    +                    copy(isWaitingFromOther = true)
    +                }
    +            }
    +            is VerificationTxState.Cancelled      -> {
    +                // The fragment should not be rendered in this state,
    +                // it should have been replaced by a conclusion fragment
    +                setState {
    +                    copy(
    +                            isWaitingFromOther = false,
    +                            supportsEmoji = sasTx.supportsEmoji(),
    +                            emojiDescription = Fail(Throwable("Transaction Cancelled")),
    +                            decimalDescription = Fail(Throwable("Transaction Cancelled"))
    +                    )
    +                }
    +            }
    +            null                                  -> {
    +                setState {
    +                    copy(
    +                            isWaitingFromOther = false,
    +                            emojiDescription = Fail(Throwable("Unknown Transaction")),
    +                            decimalDescription = Fail(Throwable("Unknown Transaction"))
    +                    )
    +                }
    +            }
    +        }
    +    }
    +
    +    override fun transactionCreated(tx: VerificationTransaction) {
    +        transactionUpdated(tx)
    +    }
    +
    +    override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
    +        if (tx.transactionId == state.transactionId && tx is SasVerificationTransaction) {
    +            refreshStateFromTx(tx)
    +        }
    +    }
    +
    +    @AssistedInject.Factory
    +    interface Factory {
    +        fun create(initialState: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel
    +    }
    +
    +    companion object : MvRxViewModelFactory {
    +
    +        override fun create(viewModelContext: ViewModelContext, state: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel? {
    +            val factory = (viewModelContext as FragmentViewModelContext).fragment().viewModelFactory
    +            return factory.create(state)
    +        }
    +
    +        override fun initialState(viewModelContext: ViewModelContext): VerificationEmojiCodeViewState? {
    +            val args = viewModelContext.args()
    +            val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
    +            val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem()
    +
    +            return VerificationEmojiCodeViewState(
    +                    transactionId = args.verificationId,
    +                    otherUser = matrixItem
    +            )
    +        }
    +    }
    +
    +    override fun handle(action: EmptyAction) {
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt
    new file mode 100644
    index 0000000000..a6b3459701
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationActionItem.kt
    @@ -0,0 +1,79 @@
    +/*
    + * 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 android.content.res.ColorStateList
    +import android.widget.ImageView
    +import android.widget.TextView
    +import androidx.annotation.DrawableRes
    +import androidx.core.view.isVisible
    +import androidx.core.widget.ImageViewCompat
    +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
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_action)
    +abstract class BottomSheetVerificationActionItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    @DrawableRes
    +    var iconRes: Int = -1
    +    @EpoxyAttribute
    +    var title: CharSequence = ""
    +    @EpoxyAttribute
    +    var subTitle: CharSequence? = null
    +    @EpoxyAttribute
    +    var titleColor: Int = 0
    +    @EpoxyAttribute
    +    var iconColor: Int = -1
    +
    +    @EpoxyAttribute
    +    lateinit var listener: () -> Unit
    +
    +    override fun bind(holder: Holder) {
    +        holder.view.setOnClickListener {
    +            listener.invoke()
    +        }
    +
    +        holder.title.text = title
    +        holder.title.setTextColor(titleColor)
    +
    +        holder.subTitle.setTextOrHide(subTitle)
    +
    +        if (iconRes != -1) {
    +            holder.icon.isVisible = true
    +            holder.icon.setImageResource(iconRes)
    +            if (iconColor != -1) {
    +                ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(iconColor))
    +            }
    +        } else {
    +            holder.icon.isVisible = false
    +        }
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val title by bind(R.id.itemVerificationActionTitle)
    +        val subTitle by bind(R.id.itemVerificationActionSubTitle)
    +        val icon by bind(R.id.itemVerificationActionIcon)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt
    new file mode 100644
    index 0000000000..5163f5e8a8
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationBigImageItem.kt
    @@ -0,0 +1,53 @@
    +/*
    + * 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 android.widget.ImageView
    +import androidx.core.view.ViewCompat
    +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
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_big_image)
    +abstract class BottomSheetVerificationBigImageItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var imageRes: Int = 0
    +
    +    @EpoxyAttribute
    +    var contentDescription: String? = null
    +
    +    override fun bind(holder: Holder) {
    +        holder.image.setImageResource(imageRes)
    +
    +        if (contentDescription == null) {
    +            ViewCompat.setImportantForAccessibility(holder.image, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO)
    +        } else {
    +            ViewCompat.setImportantForAccessibility(holder.image, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES)
    +            holder.image.contentDescription = contentDescription
    +        }
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val image by bind(R.id.itemVerificationBigImage)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt
    new file mode 100644
    index 0000000000..8d08ef0ba7
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationDecimalCodeItem.kt
    @@ -0,0 +1,42 @@
    +/*
    + * 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 android.widget.TextView
    +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
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_decimal_code)
    +abstract class BottomSheetVerificationDecimalCodeItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var code: CharSequence = ""
    +
    +    override fun bind(holder: Holder) {
    +        holder.code.text = code
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val code by bind(R.id.itemVerificationDecimalCode)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt
    new file mode 100644
    index 0000000000..6f75d91d8f
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationEmojisItem.kt
    @@ -0,0 +1,74 @@
    +/*
    + * 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 android.view.ViewGroup
    +import android.widget.TextView
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
    +import im.vector.riotx.R
    +import im.vector.riotx.core.epoxy.VectorEpoxyHolder
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +
    +/**
    + * A emoji list for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_emojis)
    +abstract class BottomSheetVerificationEmojisItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute lateinit var emojiRepresentation0: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation1: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation2: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation3: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation4: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation5: EmojiRepresentation
    +    @EpoxyAttribute lateinit var emojiRepresentation6: EmojiRepresentation
    +
    +    override fun bind(holder: Holder) {
    +        holder.emoji0View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation0.emoji
    +        holder.emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation0.nameResId)
    +
    +        holder.emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation1.emoji
    +        holder.emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation1.nameResId)
    +
    +        holder.emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation2.emoji
    +        holder.emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation2.nameResId)
    +
    +        holder.emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation3.emoji
    +        holder.emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation3.nameResId)
    +
    +        holder.emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation4.emoji
    +        holder.emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation4.nameResId)
    +
    +        holder.emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation5.emoji
    +        holder.emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation5.nameResId)
    +
    +        holder.emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation6.emoji
    +        holder.emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation6.nameResId)
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val emoji0View by bind(R.id.emoji0)
    +        val emoji1View by bind(R.id.emoji1)
    +        val emoji2View by bind(R.id.emoji2)
    +        val emoji3View by bind(R.id.emoji3)
    +        val emoji4View by bind(R.id.emoji4)
    +        val emoji5View by bind(R.id.emoji5)
    +        val emoji6View by bind(R.id.emoji6)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt
    new file mode 100644
    index 0000000000..b5314d4869
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationNoticeItem.kt
    @@ -0,0 +1,42 @@
    +/*
    + * 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 android.widget.TextView
    +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
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_notice)
    +abstract class BottomSheetVerificationNoticeItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var notice: CharSequence = ""
    +
    +    override fun bind(holder: Holder) {
    +        holder.notice.text = notice
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val notice by bind(R.id.itemVerificationNoticeText)
    +    }
    +}
    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/epoxy/BottomSheetVerificationWaitingItem.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt
    new file mode 100644
    index 0000000000..2af5ef418f
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/epoxy/BottomSheetVerificationWaitingItem.kt
    @@ -0,0 +1,42 @@
    +/*
    + * 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 android.widget.TextView
    +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
    +
    +/**
    + * A action for bottom sheet.
    + */
    +@EpoxyModelClass(layout = R.layout.item_verification_waiting)
    +abstract class BottomSheetVerificationWaitingItem : VectorEpoxyModel() {
    +
    +    @EpoxyAttribute
    +    var title: CharSequence = ""
    +
    +    override fun bind(holder: Holder) {
    +        holder.title.text = title
    +    }
    +
    +    class Holder : VectorEpoxyHolder() {
    +        val title by bind(R.id.itemVerificationWaitingTitle)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt
    new file mode 100644
    index 0000000000..f775ac7941
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherController.kt
    @@ -0,0 +1,76 @@
    +/*
    + * 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.qrconfirmation
    +
    +import com.airbnb.epoxy.EpoxyController
    +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.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
    +import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
    +import javax.inject.Inject
    +
    +class VerificationQrScannedByOtherController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider
    +) : EpoxyController() {
    +
    +    var listener: Listener? = null
    +
    +    init {
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        bottomSheetVerificationNoticeItem {
    +            id("notice")
    +            notice(stringProvider.getString(R.string.qr_code_scanned_by_other_notice))
    +        }
    +
    +        dividerItem {
    +            id("sep0")
    +        }
    +
    +        bottomSheetVerificationActionItem {
    +            id("confirm")
    +            title(stringProvider.getString(R.string.qr_code_scanned_by_other_yes))
    +            titleColor(colorProvider.getColor(R.color.riotx_accent))
    +            iconRes(R.drawable.ic_check_on)
    +            iconColor(colorProvider.getColor(R.color.riotx_accent))
    +            listener { listener?.onUserConfirmsQrCodeScanned() }
    +        }
    +
    +        dividerItem {
    +            id("sep1")
    +        }
    +
    +        bottomSheetVerificationActionItem {
    +            id("deny")
    +            title(stringProvider.getString(R.string.qr_code_scanned_by_other_no))
    +            titleColor(colorProvider.getColor(R.color.vector_error_color))
    +            iconRes(R.drawable.ic_check_off)
    +            iconColor(colorProvider.getColor(R.color.vector_error_color))
    +            listener { listener?.onUserDeniesQrCodeScanned() }
    +        }
    +    }
    +
    +    interface Listener {
    +        fun onUserConfirmsQrCodeScanned()
    +        fun onUserDeniesQrCodeScanned()
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt
    new file mode 100644
    index 0000000000..14d294a27a
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/qrconfirmation/VerificationQrScannedByOtherFragment.kt
    @@ -0,0 +1,62 @@
    +/*
    + * 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.qrconfirmation
    +
    +import android.os.Bundle
    +import android.view.View
    +import com.airbnb.mvrx.parentFragmentViewModel
    +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.features.crypto.verification.VerificationAction
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import javax.inject.Inject
    +
    +class VerificationQrScannedByOtherFragment @Inject constructor(
    +        val controller: VerificationQrScannedByOtherController
    +) : VectorBaseFragment(), VerificationQrScannedByOtherController.Listener {
    +
    +    private val sharedViewModel 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()
    +    }
    +
    +    override fun onDestroyView() {
    +        bottomSheetVerificationRecyclerView.cleanup()
    +        controller.listener = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
    +        controller.listener = this
    +    }
    +
    +    override fun onUserConfirmsQrCodeScanned() {
    +        sharedViewModel.handle(VerificationAction.OtherUserScannedSuccessfully)
    +    }
    +
    +    override fun onUserDeniesQrCodeScanned() {
    +        sharedViewModel.handle(VerificationAction.OtherUserDidNotScanned)
    +    }
    +}
    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
    new file mode 100644
    index 0000000000..9c4a5a870f
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt
    @@ -0,0 +1,116 @@
    +/*
    + * 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 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
    +import javax.inject.Inject
    +
    +class VerificationRequestController @Inject constructor(
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider
    +) : EpoxyController() {
    +
    +    var listener: Listener? = null
    +
    +    private var viewState: VerificationBottomSheetViewState? = null
    +
    +    fun update(viewState: VerificationBottomSheetViewState) {
    +        this.viewState = viewState
    +        requestModelBuild()
    +    }
    +
    +    override fun buildModels() {
    +        val state = viewState ?: return
    +        val matrixItem = viewState?.otherUserMxItem ?: return
    +
    +        if (state.waitForOtherUserMode) {
    +            bottomSheetVerificationNoticeItem {
    +                id("notice")
    +                notice(stringProvider.getString(R.string.verification_open_other_to_verify))
    +            }
    +
    +            dividerItem {
    +                id("sep")
    +            }
    +
    +            bottomSheetVerificationWaitingItem {
    +                id("waiting")
    +                title(stringProvider.getString(R.string.verification_request_waiting_for, matrixItem.getBestName()))
    +            }
    +        } else {
    +            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))
    +            }
    +
    +            bottomSheetVerificationNoticeItem {
    +                id("notice")
    +                notice(styledText)
    +            }
    +
    +            dividerItem {
    +                id("sep")
    +            }
    +
    +            when (val pr = state.pendingRequest) {
    +                is Uninitialized -> {
    +                    bottomSheetVerificationActionItem {
    +                        id("start")
    +                        title(stringProvider.getString(R.string.start_verification))
    +                        titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                        subTitle(stringProvider.getString(R.string.verification_request_start_notice))
    +                        iconRes(R.drawable.ic_arrow_right)
    +                        iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
    +                        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()))
    +                        }
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    interface Listener {
    +        fun onClickOnVerificationStart()
    +    }
    +}
    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
    new file mode 100644
    index 0000000000..8231242d08
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
    @@ -0,0 +1,64 @@
    +/*
    + * 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 android.os.Bundle
    +import android.view.View
    +import com.airbnb.mvrx.parentFragmentViewModel
    +import com.airbnb.mvrx.withState
    +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.features.crypto.verification.VerificationAction
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import javax.inject.Inject
    +
    +class VerificationRequestFragment @Inject constructor(
    +        val controller: VerificationRequestController
    +) : VectorBaseFragment(), VerificationRequestController.Listener {
    +
    +    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()
    +    }
    +
    +    override fun onDestroyView() {
    +        bottomSheetVerificationRecyclerView.cleanup()
    +        controller.listener = null
    +        super.onDestroyView()
    +    }
    +
    +    private fun setupRecyclerView() {
    +        bottomSheetVerificationRecyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
    +        controller.listener = this
    +    }
    +
    +    override fun invalidate() = withState(viewModel) { state ->
    +        controller.update(state)
    +    }
    +
    +    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/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    index 1102c67e16..94e14ab214 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
    @@ -22,10 +22,15 @@ import android.os.Bundle
     import android.view.MenuItem
     import androidx.appcompat.app.AlertDialog
     import androidx.appcompat.widget.Toolbar
    +import androidx.core.content.ContextCompat
     import androidx.core.view.GravityCompat
     import androidx.core.view.isVisible
     import androidx.drawerlayout.widget.DrawerLayout
     import androidx.lifecycle.Observer
    +import im.vector.matrix.android.api.MatrixCallback
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
    +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
     import im.vector.riotx.R
     import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.di.ScreenComponent
    @@ -36,6 +41,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
     import im.vector.riotx.core.pushers.PushersManager
     import im.vector.riotx.features.disclaimer.showDisclaimerDialog
     import im.vector.riotx.features.notifications.NotificationDrawerManager
    +import im.vector.riotx.features.popup.PopupAlertManager
     import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
     import im.vector.riotx.features.workers.signout.SignOutViewModel
     import im.vector.riotx.push.fcm.FcmHelper
    @@ -96,7 +102,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
             activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
                 if (status == null) {
                     waiting_view.isVisible = false
    +                promptCompleteSecurityIfNeeded()
                 } else {
    +                sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = false
                     Timber.v("${getString(status.statusText)} ${status.percentProgress}")
                     waiting_view.setOnClickListener {
                         // block interactions
    @@ -116,6 +124,65 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
             })
         }
     
    +    private fun promptCompleteSecurityIfNeeded() {
    +        val session = activeSessionHolder.getSafeActiveSession() ?: return
    +        if (!session.hasAlreadySynced()) return
    +        if (sharedActionViewModel.hasDisplayedCompleteSecurityPrompt) return
    +
    +        // ensure keys are downloaded
    +        session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> {
    +            override fun onSuccess(data: MXUsersDevicesMap) {
    +                runOnUiThread {
    +                    alertCompleteSecurity(session)
    +                }
    +            }
    +        })
    +    }
    +
    +    private fun alertCompleteSecurity(session: Session) {
    +        val myCrossSigningKeys = session.getCrossSigningService()
    +                .getMyCrossSigningKeys()
    +        val crossSigningEnabledOnAccount = myCrossSigningKeys != null
    +
    +        if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
    +            // We need to ask
    +            sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
    +            PopupAlertManager.postVectorAlert(
    +                    PopupAlertManager.VectorAlert(
    +                            uid = "completeSecurity",
    +                            title = getString(R.string.crosssigning_verify_this_session),
    +                            description = getString(R.string.crosssigning_other_user_not_trust),
    +                            iconId = R.drawable.ic_shield_warning
    +                    ).apply {
    +                        colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
    +                        contentAction = Runnable {
    +                            Runnable {
    +                                (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                    it.navigator.waitSessionVerification(it)
    +                                }
    +                            }
    +                        }
    +                        dismissedAction = Runnable {
    +                            //                            tx.cancel()
    +                        }
    +                        addButton(
    +                                getString(R.string.later),
    +                                Runnable {
    +                                }
    +                        )
    +                        addButton(
    +                                getString(R.string.verification_profile_verify),
    +                                Runnable {
    +                                    (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
    +                                        it.navigator.waitSessionVerification(it)
    +                                    }
    +                                }
    +                        )
    +                    }
    +            )
    +        }
    +    }
    +
         override fun onNewIntent(intent: Intent?) {
             super.onNewIntent(intent)
             if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt
    index cd81448a0a..ecbe460b90 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt
    @@ -19,4 +19,6 @@ package im.vector.riotx.features.home
     import im.vector.riotx.core.platform.VectorSharedActionViewModel
     import javax.inject.Inject
     
    -class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel()
    +class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() {
    +    var hasDisplayedCompleteSecurityPrompt : Boolean = false
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    index 8024e5b547..d0716bd047 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
    @@ -64,4 +64,9 @@ sealed class RoomDetailAction : VectorViewModelAction {
     
         object ClearSendQueue : RoomDetailAction()
         object ResendAll : RoomDetailAction()
    +
    +    data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
    +    data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
    +    data class RequestVerification(val userId: String) : RoomDetailAction()
    +    data class ResumeVerification(val transactionId: String, val otherUserId: String? = null, val otherdDeviceId: String? = null) : RoomDetailAction()
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
    index 14e9061c36..fe4d0ae1f7 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
    @@ -109,7 +109,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
     
         companion object {
     
    -        private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"
    +        const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"
     
             fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent {
                 return Intent(context, RoomDetailActivity::class.java).apply {
    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 950b865a36..e9e20eba8f 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
    @@ -54,6 +54,7 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener
     import com.airbnb.mvrx.Async
     import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.Loading
    +import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.args
     import com.airbnb.mvrx.fragmentViewModel
    @@ -74,6 +75,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageFileConten
     import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
     import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
     import im.vector.matrix.android.api.session.room.model.message.MessageType
    +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
     import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
     import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.Timeline
    @@ -114,6 +116,8 @@ import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
     import im.vector.riotx.features.attachments.AttachmentsHelper
     import im.vector.riotx.features.attachments.ContactAttachment
     import im.vector.riotx.features.command.Command
    +import im.vector.riotx.features.crypto.util.toImageRes
    +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.detail.composer.TextComposerView
     import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
    @@ -679,18 +683,23 @@ class RoomDetailFragment @Inject constructor(
         }
     
         private fun renderRoomSummary(state: RoomDetailViewState) {
    -        state.asyncRoomSummary()?.let {
    -            if (it.membership.isLeft()) {
    +        state.asyncRoomSummary()?.let { roomSummary ->
    +            if (roomSummary.membership.isLeft()) {
                     Timber.w("The room has been left")
                     activity?.finish()
                 } else {
    -                roomToolbarTitleView.text = it.displayName
    -                avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
    +                roomToolbarTitleView.text = roomSummary.displayName
    +                avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
     
    -                renderSubTitle(state.typingMessage, it.topic)
    +                renderSubTitle(state.typingMessage, roomSummary.topic)
    +            }
    +            jumpToBottomView.count = roomSummary.notificationCount
    +            jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages
    +
    +            roomToolbarDecorationImageView.let {
    +                it.setImageResource(roomSummary.roomEncryptionTrustLevel.toImageRes())
    +                it.isVisible = roomSummary.roomEncryptionTrustLevel != null
                 }
    -            jumpToBottomView.count = it.notificationCount
    -            jumpToBottomView.drawBadge = it.hasUnreadMessages
             }
         }
     
    @@ -793,7 +802,7 @@ class RoomDetailFragment @Inject constructor(
                 }
                 is Success -> {
                     when (val data = result.invoke()) {
    -                    is RoomDetailAction.ReportContent -> {
    +                    is RoomDetailAction.ReportContent             -> {
                             when {
                                 data.spam          -> {
                                     AlertDialog.Builder(requireActivity())
    @@ -830,6 +839,30 @@ class RoomDetailFragment @Inject constructor(
                                 }
                             }
                         }
    +                    is RoomDetailAction.RequestVerification       -> {
    +                        Timber.v("## SAS RequestVerification action")
    +                        VerificationBottomSheet.withArgs(
    +                                roomDetailArgs.roomId,
    +                                data.userId
    +                        ).show(parentFragmentManager, "REQ")
    +                    }
    +                    is RoomDetailAction.AcceptVerificationRequest -> {
    +                        Timber.v("## SAS AcceptVerificationRequest action")
    +                        VerificationBottomSheet.withArgs(
    +                                roomDetailArgs.roomId,
    +                                data.otherUserId,
    +                                data.transactionId
    +                        ).show(parentFragmentManager, "REQ")
    +                    }
    +                    is RoomDetailAction.ResumeVerification        -> {
    +                        val otherUserId = data.otherUserId ?: return
    +                        VerificationBottomSheet().apply {
    +                            arguments = Bundle().apply {
    +                                putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
    +                                        otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
    +                            }
    +                        }.show(parentFragmentManager, "REQ")
    +                    }
                     }
                 }
             }
    @@ -979,6 +1012,9 @@ class RoomDetailFragment @Inject constructor(
         }
     
         override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
    +        if (messageContent is MessageVerificationRequestContent) {
    +            roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId))
    +        }
         }
     
         override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean {
    @@ -994,6 +1030,7 @@ class RoomDetailFragment @Inject constructor(
         }
     
         override fun onAvatarClicked(informationData: MessageInformationData) {
    +        // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
             openRoomMemberProfile(informationData.senderId)
         }
     
    @@ -1025,6 +1062,10 @@ class RoomDetailFragment @Inject constructor(
                     .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
         }
     
    +    override fun onTimelineItemAction(itemAction: RoomDetailAction) {
    +        roomDetailViewModel.handle(itemAction)
    +    }
    +
         override fun onRoomCreateLinkClicked(url: String) {
             permalinkHandler
                     .launch(requireContext(), url, object : NavigationInterceptor {
    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 51808567cb..91e8ab5300 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
    @@ -71,6 +71,7 @@ import im.vector.riotx.core.utils.subscribeLogError
     import im.vector.riotx.features.command.CommandParser
     import im.vector.riotx.features.command.ParsedCommand
     import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
    +import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
     import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
     import im.vector.riotx.features.home.room.typing.TypingHelper
     import im.vector.riotx.features.settings.VectorPreferences
    @@ -157,8 +158,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             observeUnreadState()
             observeMyRoomMember()
             room.getRoomSummaryLive()
    -        room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
             room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, object : MatrixCallback {})
    +        room.rx(session).loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
             // Inform the SDK that the room is displayed
             session.onRoomDisplayed(initialState.roomId)
         }
    @@ -167,7 +168,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             val queryParams = roomMemberQueryParams {
                 this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE)
             }
    -        room.rx()
    +        room.rx(session)
                     .liveRoomMembers(queryParams)
                     .map {
                         it.firstOrNull().toOptional()
    @@ -209,6 +210,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 is RoomDetailAction.IgnoreUser                       -> handleIgnoreUser(action)
                 is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
                 is RoomDetailAction.ExitTrackingUnreadMessagesState  -> stopTrackingUnreadMessages()
    +            is RoomDetailAction.AcceptVerificationRequest        -> handleAcceptVerification(action)
    +            is RoomDetailAction.DeclineVerificationRequest       -> handleDeclineVerification(action)
    +            is RoomDetailAction.RequestVerification              -> handleRequestVerification(action)
    +            is RoomDetailAction.ResumeVerification               -> handleResumeRequestVerification(action)
             }
         }
     
    @@ -250,7 +255,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun observeDrafts() {
    -        room.rx().liveDrafts()
    +        room.rx(session).liveDrafts()
                     .subscribe {
                         Timber.d("Draft update --> SetState")
                         setState {
    @@ -432,6 +437,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                                 _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
                                 popDraft()
                             }
    +                        is ParsedCommand.SendShrug                -> {
    +                            val sequence = buildString {
    +                                append("¯\\_(ツ)_/¯")
    +                                if (slashCommandResult.message.isNotEmpty()) {
    +                                    append(" ")
    +                                    append(slashCommandResult.message)
    +                                }
    +                            }
    +                            room.sendTextMessage(sequence)
    +                            _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
    +                            popDraft()
    +                        }
    +                        is ParsedCommand.VerifyUser               -> {
    +                            session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
    +                            _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
    +                            popDraft()
    +                        }
                             is ParsedCommand.ChangeTopic              -> {
                                 handleChangeTopicSlashCommand(slashCommandResult)
                                 popDraft()
    @@ -824,6 +846,44 @@ 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.getVerificationService().readyPendingVerificationInDMs(
    +                        supportedVerificationMethods,
    +                        action.otherUserId,
    +                        room.roomId,
    +                        action.transactionId)) {
    +            _requestLiveData.postValue(LiveEvent(Success(action)))
    +        } else {
    +            // TODO
    +        }
    +    }
    +
    +    private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
    +        session.getVerificationService().declineVerificationRequestInDMs(
    +                action.otherUserId,
    +                action.otherdDeviceId,
    +                action.transactionId,
    +                room.roomId)
    +    }
    +
    +    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.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
    +            if (it.handledByOtherSession) return
    +            if (!it.isFinished) {
    +                _requestLiveData.postValue(LiveEvent(Success(action.copy(
    +                        otherUserId = it.otherUserId
    +                ))))
    +            }
    +        }
    +    }
    +
         private fun observeSyncState() {
             session.rx()
                     .liveSyncState()
    @@ -836,7 +896,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     
         private fun observeRoomSummary() {
    -        room.rx().liveRoomSummary()
    +        room.rx(session).liveRoomSummary()
                     .unwrap()
                     .execute { async ->
                         val typingRoomMembers =
    @@ -854,7 +914,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             Observable
                     .combineLatest, RoomSummary, UnreadState>(
                             timelineEvents.observeOn(Schedulers.computation()),
    -                        room.rx().liveRoomSummary().unwrap(),
    +                        room.rx(session).liveRoomSummary().unwrap(),
                             BiFunction { timelineEvents, roomSummary ->
                                 computeUnreadState(timelineEvents, roomSummary)
                             }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    index 4827c825cb..fe05d032ea 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt
    @@ -18,12 +18,8 @@ package im.vector.riotx.features.home.room.detail.readreceipts
     
     import android.os.Bundle
     import android.os.Parcelable
    -import android.view.LayoutInflater
    -import android.view.View
    -import android.view.ViewGroup
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    -import butterknife.ButterKnife
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.args
     import im.vector.riotx.R
    @@ -57,11 +53,7 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
             injector.inject(this)
         }
     
    -    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
    -        ButterKnife.bind(this, view)
    -        return view
    -    }
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    index cdc75477fb..c76948a557 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
    @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.riotx.core.date.VectorDateFormatter
     import im.vector.riotx.core.epoxy.LoadingItem_
     import im.vector.riotx.core.extensions.localDateTime
    +import im.vector.riotx.features.home.room.detail.RoomDetailAction
     import im.vector.riotx.features.home.room.detail.RoomDetailViewState
     import im.vector.riotx.features.home.room.detail.UnreadState
     import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
    @@ -62,6 +63,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
             fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
             fun onEditedDecorationClicked(informationData: MessageInformationData)
    +
    +        // TODO move all callbacks to this?
    +        fun onTimelineItemAction(itemAction: RoomDetailAction)
         }
     
         interface ReactionPillCallback {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    index ba772344e0..1336c61b68 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
    @@ -16,12 +16,8 @@
     package im.vector.riotx.features.home.room.detail.timeline.action
     
     import android.os.Bundle
    -import android.view.LayoutInflater
    -import android.view.View
    -import android.view.ViewGroup
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    -import butterknife.ButterKnife
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
     import im.vector.riotx.R
    @@ -53,18 +49,12 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
             injector.inject(this)
         }
     
    -    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false)
    -        ButterKnife.bind(this, view)
    -        return view
    -    }
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
             sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
    -        recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false)
    -        // Disable item animation
    -        recyclerView.itemAnimator = null
    +        recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false, disableItemAnimation = true)
             messageActionsEpoxyController.listener = this
         }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    index 936bc263e7..aa81658bf9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
    @@ -28,10 +28,7 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.events.model.EventType
     import im.vector.matrix.android.api.session.events.model.isTextMessage
     import im.vector.matrix.android.api.session.events.model.toModel
    -import im.vector.matrix.android.api.session.room.model.message.MessageContent
    -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
    -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
    -import im.vector.matrix.android.api.session.room.model.message.MessageType
    +import im.vector.matrix.android.api.session.room.model.message.*
     import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
    @@ -137,7 +134,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     
         private fun observeEvent() {
             if (room == null) return
    -        room.rx()
    +        room.rx(session)
                     .liveTimelineEvent(eventId)
                     .unwrap()
                     .execute {
    @@ -147,7 +144,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
     
         private fun observeReactions() {
             if (room == null) return
    -        room.rx()
    +        room.rx(session)
                     .liveAnnotationSummary(eventId)
                     .map { annotations ->
                         EmojiDataSource.quickEmojis.map { emoji ->
    @@ -182,6 +179,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
                                 ?: messageContent.body
     
                         eventHtmlRenderer.get().render(html)
    +                } else if (messageContent is MessageVerificationRequestContent) {
    +                    stringProvider.getString(R.string.verification_request)
                     } else {
                         messageContent?.body
                     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    index 8aa7c8561c..bf3816a030 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt
    @@ -16,12 +16,8 @@
     package im.vector.riotx.features.home.room.detail.timeline.edithistory
     
     import android.os.Bundle
    -import android.view.LayoutInflater
    -import android.view.View
    -import android.view.ViewGroup
     import androidx.recyclerview.widget.RecyclerView
     import butterknife.BindView
    -import butterknife.ButterKnife
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.fragmentViewModel
     import com.airbnb.mvrx.withState
    @@ -57,11 +53,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
             injector.inject(this)
         }
     
    -    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    -        val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
    -        ButterKnife.bind(this, view)
    -        return view
    -    }
    +    override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
     
         override fun onActivityCreated(savedInstanceState: Bundle?) {
             super.onActivityCreated(savedInstanceState)
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    index 3febf19208..086dfe3754 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
    @@ -24,6 +24,7 @@ import android.text.style.ClickableSpan
     import android.text.style.ForegroundColorSpan
     import android.view.View
     import dagger.Lazy
    +import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.events.model.RelationType
     import im.vector.matrix.android.api.session.events.model.toModel
     import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
    @@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoC
     import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
     import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
     import im.vector.matrix.android.api.session.room.model.message.MessageType
    +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
     import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
     import im.vector.matrix.android.api.session.room.model.message.getFileUrl
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    @@ -67,6 +69,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
     import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
     import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_
    +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestItem
    +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestItem_
     import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
     import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
     import im.vector.riotx.features.html.CodeVisitor
    @@ -91,7 +95,8 @@ class MessageItemFactory @Inject constructor(
             private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
             private val defaultItemFactory: DefaultItemFactory,
             private val noticeItemFactory: NoticeItemFactory,
    -        private val avatarSizeProvider: AvatarSizeProvider) {
    +        private val avatarSizeProvider: AvatarSizeProvider,
    +        private val session: Session) {
     
         fun create(event: TimelineEvent,
                    nextEvent: TimelineEvent?,
    @@ -124,14 +129,15 @@ class MessageItemFactory @Inject constructor(
     //        val all = event.root.toContent()
     //        val ev = all.toModel()
             return when (messageContent) {
    -            is MessageEmoteContent     -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageTextContent      -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
    -            is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageNoticeContent    -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageVideoContent     -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageFileContent      -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            is MessageAudioContent     -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
    -            else                       -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
    +            is MessageEmoteContent               -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageTextContent                -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
    +            is MessageImageInfoContent           -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageNoticeContent              -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageVideoContent               -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageFileContent                -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageAudioContent               -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
    +            else                                 -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
             }
         }
     
    @@ -155,6 +161,51 @@ class MessageItemFactory @Inject constructor(
                             }))
         }
     
    +    private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
    +                                                    @Suppress("UNUSED_PARAMETER")
    +                                                    informationData: MessageInformationData,
    +                                                    highlight: Boolean,
    +                                                    callback: TimelineEventController.Callback?,
    +                                                    attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
    +        // If this request is not sent by me or sent to me, we should ignore it in timeline
    +        val myUserId = session.myUserId
    +        if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
    +            return null
    +        }
    +
    +        val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
    +        val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
    +        else informationData.memberName
    +        return VerificationRequestItem_()
    +                .attributes(
    +                        VerificationRequestItem.Attributes(
    +                                otherUserId = otherUserId,
    +                                otherUserName = otherUserName.toString(),
    +                                fromDevide = messageContent.fromDevice ?: "",
    +                                referenceId = informationData.eventId,
    +                                informationData = informationData,
    +                                avatarRenderer = attributes.avatarRenderer,
    +                                colorProvider = attributes.colorProvider,
    +                                itemLongClickListener = attributes.itemLongClickListener,
    +                                itemClickListener = attributes.itemClickListener,
    +                                reactionPillCallback = attributes.reactionPillCallback,
    +                                readReceiptsCallback = attributes.readReceiptsCallback,
    +                                emojiTypeFace = attributes.emojiTypeFace
    +                        )
    +                )
    +                .callback(callback)
    +//                .izLocalFile(messageContent.getFileUrl().isLocalFile())
    +//                .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
    +                .highlighted(highlight)
    +                .leftGuideline(avatarSizeProvider.leftGuideline)
    +//                .filename(messageContent.body)
    +//                .iconRes(R.drawable.filetype_audio)
    +//                .clickListener(
    +//                        DebouncedClickListener(View.OnClickListener {
    +//                            callback?.onAudioMessageClicked(messageContent)
    +//                        }))
    +    }
    +
         private fun buildFileMessageItem(messageContent: MessageFileContent,
                                          informationData: MessageInformationData,
                                          highlight: Boolean,
    @@ -226,7 +277,8 @@ class MessageItemFactory @Inject constructor(
             val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
             val thumbnailData = ImageContentRenderer.Data(
                     filename = messageContent.body,
    -                url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
    +                url = messageContent.videoInfo?.thumbnailFile?.url
    +                        ?: messageContent.videoInfo?.thumbnailUrl,
                     elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
                     height = messageContent.videoInfo?.height,
                     maxHeight = maxHeight,
    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 15df3bc5c9..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,16 +20,18 @@ 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
     
    -class TimelineItemFactory @Inject constructor(
    -        private val messageItemFactory: MessageItemFactory,
    -        private val encryptedItemFactory: EncryptedItemFactory,
    -        private val noticeItemFactory: NoticeItemFactory,
    -        private val defaultItemFactory: DefaultItemFactory,
    -        private val roomCreateItemFactory: RoomCreateItemFactory) {
    +class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory,
    +                                              private val encryptedItemFactory: EncryptedItemFactory,
    +                                              private val noticeItemFactory: NoticeItemFactory,
    +                                              private val defaultItemFactory: DefaultItemFactory,
    +                                              private val roomCreateItemFactory: RoomCreateItemFactory,
    +                                              private val verificationConclusionItemFactory: VerificationItemFactory,
    +                                              private val userPreferencesProvider: UserPreferencesProvider) {
     
         fun create(event: TimelineEvent,
                    nextEvent: TimelineEvent?,
    @@ -68,6 +70,22 @@ class TimelineItemFactory @Inject constructor(
                             encryptedItemFactory.create(event, nextEvent, highlight, callback)
                         }
                     }
    +                EventType.KEY_VERIFICATION_ACCEPT,
    +                EventType.KEY_VERIFICATION_START,
    +                EventType.KEY_VERIFICATION_KEY,
    +                EventType.KEY_VERIFICATION_READY,
    +                EventType.KEY_VERIFICATION_MAC          -> {
    +                    // 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         -> {
    +                    verificationConclusionItemFactory.create(event, highlight, callback)
    +                }
    +
                     // Unhandled event types (yet)
                     EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
                     else                                    -> {
    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
    new file mode 100644
    index 0000000000..dc5bd740dd
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    @@ -0,0 +1,155 @@
    +/*
    + * 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.home.room.detail.timeline.factory
    +
    +import im.vector.matrix.android.api.session.Session
    +import im.vector.matrix.android.api.session.crypto.sas.CancelCode
    +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
    +import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.events.model.RelationType
    +import im.vector.matrix.android.api.session.events.model.toModel
    +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
    +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
    +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    +import im.vector.matrix.android.internal.session.room.VerificationState
    +import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.UserPreferencesProvider
    +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
    +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
    +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
    +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
    +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
    +import javax.inject.Inject
    +
    +/**
    + * Can creates verification conclusion items
    + * Notice that not all KEY_VERIFICATION_DONE will be displayed in timeline,
    + * several checks are made to see if this conclusion is attached to a known request
    + */
    +class VerificationItemFactory @Inject constructor(
    +        private val colorProvider: ColorProvider,
    +        private val messageInformationDataFactory: MessageInformationDataFactory,
    +        private val messageItemAttributesFactory: MessageItemAttributesFactory,
    +        private val avatarSizeProvider: AvatarSizeProvider,
    +        private val noticeItemFactory: NoticeItemFactory,
    +        private val userPreferencesProvider: UserPreferencesProvider,
    +        private val session: Session
    +) {
    +
    +    fun create(event: TimelineEvent,
    +               highlight: Boolean,
    +               callback: TimelineEventController.Callback?
    +    ): VectorEpoxyModel<*>? {
    +        if (event.root.eventId == null) return null
    +
    +        val relContent: MessageRelationContent = event.root.content.toModel()
    +                ?: event.root.getClearContent().toModel()
    +                ?: return ignoredConclusion(event, highlight, callback)
    +
    +        if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback)
    +        val refEventId = relContent.relatesTo?.eventId
    +                ?: return ignoredConclusion(event, highlight, callback)
    +
    +        // If we cannot find the referenced request we do not display the done event
    +        val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
    +                ?: return ignoredConclusion(event, highlight, callback)
    +
    +        // If it's not a request ignore this event
    +        if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback)
    +
    +        val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
    +
    +        val informationData = messageInformationDataFactory.create(event, null)
    +        val attributes = messageItemAttributesFactory.create(null, informationData, callback)
    +
    +        when (event.root.getClearType()) {
    +            EventType.KEY_VERIFICATION_CANCEL -> {
    +                // Is the request referenced is actually really cancelled?
    +                val cancelContent = event.root.getClearContent().toModel()
    +                        ?: return ignoredConclusion(event, highlight, callback)
    +
    +                when (safeValueOf(cancelContent.code)) {
    +                    CancelCode.MismatchedCommitment,
    +                    CancelCode.MismatchedKeys,
    +                    CancelCode.MismatchedSas -> {
    +                        // We should display these bad conclusions
    +                        return VerificationRequestConclusionItem_()
    +                                .attributes(
    +                                        VerificationRequestConclusionItem.Attributes(
    +                                                toUserId = informationData.senderId,
    +                                                toUserName = informationData.memberName.toString(),
    +                                                isPositive = false,
    +                                                informationData = informationData,
    +                                                avatarRenderer = attributes.avatarRenderer,
    +                                                colorProvider = colorProvider,
    +                                                emojiTypeFace = attributes.emojiTypeFace,
    +                                                itemClickListener = attributes.itemClickListener,
    +                                                itemLongClickListener = attributes.itemLongClickListener,
    +                                                reactionPillCallback = attributes.reactionPillCallback,
    +                                                readReceiptsCallback = attributes.readReceiptsCallback
    +                                        )
    +                                )
    +                                .highlighted(highlight)
    +                                .leftGuideline(avatarSizeProvider.leftGuideline)
    +                    }
    +                    else                     -> return ignoredConclusion(event, highlight, callback)
    +                }
    +            }
    +            EventType.KEY_VERIFICATION_DONE   -> {
    +                // Is the request referenced is actually really completed?
    +                if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) {
    +                    return ignoredConclusion(event, highlight, callback)
    +                }
    +                // We only tale the one sent by me
    +
    +                if (informationData.sentByMe) {
    +                    // We only display the done sent by the other user, the done send by me is ignored
    +                    return ignoredConclusion(event, highlight, callback)
    +                }
    +                return VerificationRequestConclusionItem_()
    +                        .attributes(
    +                                VerificationRequestConclusionItem.Attributes(
    +                                        toUserId = informationData.senderId,
    +                                        toUserName = informationData.memberName.toString(),
    +                                        isPositive = true,
    +                                        informationData = informationData,
    +                                        avatarRenderer = attributes.avatarRenderer,
    +                                        colorProvider = colorProvider,
    +                                        emojiTypeFace = attributes.emojiTypeFace,
    +                                        itemClickListener = attributes.itemClickListener,
    +                                        itemLongClickListener = attributes.itemLongClickListener,
    +                                        reactionPillCallback = attributes.reactionPillCallback,
    +                                        readReceiptsCallback = attributes.readReceiptsCallback
    +                                )
    +                        )
    +                        .highlighted(highlight)
    +                        .leftGuideline(avatarSizeProvider.leftGuideline)
    +            }
    +        }
    +        return null
    +    }
    +
    +    private fun ignoredConclusion(event: TimelineEvent,
    +                                  highlight: Boolean,
    +                                  callback: TimelineEventController.Callback?
    +    ): VectorEpoxyModel<*>? {
    +        if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
    +        return null
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
    new file mode 100644
    index 0000000000..ed6bc9df62
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
    @@ -0,0 +1,104 @@
    +/*
    + * 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.home.room.detail.timeline.format
    +
    +import im.vector.matrix.android.api.session.events.model.EventType
    +import im.vector.matrix.android.api.session.room.model.message.MessageType
    +import im.vector.matrix.android.api.session.room.model.message.isReply
    +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
    +import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
    +import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
    +import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.resources.StringProvider
    +import me.gujun.android.span.span
    +import javax.inject.Inject
    +
    +class DisplayableEventFormatter @Inject constructor(
    +//        private val sessionHolder: ActiveSessionHolder,
    +        private val stringProvider: StringProvider,
    +        private val colorProvider: ColorProvider,
    +        private val noticeEventFormatter: NoticeEventFormatter
    +) {
    +
    +    fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
    +        if (timelineEvent.root.isEncrypted()
    +                && timelineEvent.root.mxDecryptionResult == null) {
    +            return stringProvider.getString(R.string.encrypted_message)
    +        }
    +
    +        val senderName = timelineEvent.getDisambiguatedDisplayName()
    +
    +        when (timelineEvent.root.getClearType()) {
    +            EventType.MESSAGE -> {
    +                timelineEvent.getLastMessageContent()?.let { messageContent ->
    +                    when (messageContent.type) {
    +                        MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
    +                            return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
    +                        }
    +                        MessageType.MSGTYPE_IMAGE                -> {
    +                            return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
    +                        }
    +                        MessageType.MSGTYPE_AUDIO                -> {
    +                            return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
    +                        }
    +                        MessageType.MSGTYPE_VIDEO                -> {
    +                            return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
    +                        }
    +                        MessageType.MSGTYPE_FILE                 -> {
    +                            return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
    +                        }
    +                        MessageType.MSGTYPE_TEXT                 -> {
    +                            if (messageContent.isReply()) {
    +                                // Skip reply prefix, and show important
    +                                // TODO add a reply image span ?
    +                                return simpleFormat(senderName, timelineEvent.getTextEditableContent()
    +                                        ?: messageContent.body, appendAuthor)
    +                            } else {
    +                                return simpleFormat(senderName, messageContent.body, appendAuthor)
    +                            }
    +                        }
    +                        else                                     -> {
    +                            return simpleFormat(senderName, messageContent.body, appendAuthor)
    +                        }
    +                    }
    +                }
    +            }
    +            else              -> {
    +                return span {
    +                    text = noticeEventFormatter.format(timelineEvent) ?: ""
    +                    textStyle = "italic"
    +                }
    +            }
    +        }
    +
    +        return span { }
    +    }
    +
    +    private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
    +        return if (appendAuthor) {
    +            span {
    +                text = senderName
    +                textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)
    +            }
    +                    .append(": ")
    +                    .append(body)
    +        } else {
    +            body
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    index 16275e6a73..8d70279fce 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    @@ -61,6 +61,13 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                 EventType.CALL_ANSWER                   -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
                 EventType.MESSAGE,
                 EventType.REACTION,
    +            EventType.KEY_VERIFICATION_START,
    +            EventType.KEY_VERIFICATION_CANCEL,
    +            EventType.KEY_VERIFICATION_ACCEPT,
    +            EventType.KEY_VERIFICATION_MAC,
    +            EventType.KEY_VERIFICATION_DONE,
    +            EventType.KEY_VERIFICATION_KEY,
    +            EventType.KEY_VERIFICATION_READY,
                 EventType.REDACTION                     -> formatDebug(timelineEvent.root)
                 else                                    -> {
                     Timber.v("Type $type not handled by this formatter")
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    index 0a8f1e11bb..cbbfd7c320 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
    @@ -20,15 +20,19 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
     
     import im.vector.matrix.android.api.session.Session
     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.ReferencesAggregatedContent
     import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
    +import im.vector.matrix.android.internal.session.room.VerificationState
    +import im.vector.riotx.core.date.VectorDateFormatter
     import im.vector.riotx.core.extensions.localDateTime
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.utils.getColorFromUserId
    -import im.vector.riotx.core.date.VectorDateFormatter
     import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
     import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
     import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
    +import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData
     import me.gujun.android.span.span
     import javax.inject.Inject
     
    @@ -69,6 +73,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
                     senderId = event.root.senderId ?: "",
                     sendState = event.root.sendState,
                     time = time,
    +                ageLocalTS = event.root.ageLocalTs,
                     avatarUrl = avatarUrl,
                     memberName = formattedMemberName,
                     showInformation = showInformation,
    @@ -87,7 +92,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
                             .map {
                                 ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
                             }
    -                        .toList()
    +                        .toList(),
    +                referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
    +                    val stateStr = referencesAggregatedSummary.content.toModel()?.verificationSummary
    +                    ReferencesInfoData(
    +                            VerificationState.values().firstOrNull { stateStr == it.name }
    +                                    ?: VerificationState.REQUEST
    +                    )
    +                },
    +                sentByMe = event.root.senderId == session.myUserId
             )
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
    index 8d85ffb676..5c763cb114 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
    @@ -40,7 +40,9 @@ object TimelineDisplayableEvents {
                 EventType.STICKER,
                 EventType.STATE_ROOM_CREATE,
                 EventType.STATE_ROOM_TOMBSTONE,
    -            EventType.STATE_ROOM_JOIN_RULES
    +            EventType.STATE_ROOM_JOIN_RULES,
    +            EventType.KEY_VERIFICATION_DONE,
    +            EventType.KEY_VERIFICATION_CANCEL
         )
     }
     
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
    new file mode 100644
    index 0000000000..6d99bb2650
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
    @@ -0,0 +1,142 @@
    +/*
    + * 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.home.room.detail.timeline.item
    +
    +import android.view.View
    +import android.view.ViewGroup
    +import android.widget.ImageView
    +import android.widget.TextView
    +import androidx.annotation.IdRes
    +import androidx.core.view.isVisible
    +import im.vector.matrix.android.api.session.room.send.SendState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.utils.DebouncedClickListener
    +import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    +import im.vector.riotx.features.reactions.widget.ReactionButton
    +import im.vector.riotx.features.ui.getMessageTextColor
    +
    +/**
    + * Base timeline item with reactions and read receipts.
    + * Manages associated click listeners and send status.
    + * Should not be used as this, use a subclass.
    + */
    +abstract class AbsBaseMessageItem : BaseEventItem() {
    +
    +    abstract val baseAttributes: Attributes
    +
    +    private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
    +        baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
    +    })
    +
    +    private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
    +        override fun onReacted(reactionButton: ReactionButton) {
    +            baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
    +        }
    +
    +        override fun onUnReacted(reactionButton: ReactionButton) {
    +            baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, false)
    +        }
    +
    +        override fun onLongClick(reactionButton: ReactionButton) {
    +            baseAttributes.reactionPillCallback?.onLongClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString)
    +        }
    +    }
    +
    +    open fun shouldShowReactionAtBottom(): Boolean {
    +        return true
    +    }
    +
    +    override fun getEventIds(): List {
    +        return listOf(baseAttributes.informationData.eventId)
    +    }
    +
    +    override fun bind(holder: H) {
    +        super.bind(holder)
    +        holder.readReceiptsView.render(
    +                baseAttributes.informationData.readReceipts,
    +                baseAttributes.avatarRenderer,
    +                _readReceiptsClickListener
    +        )
    +
    +        val reactions = baseAttributes.informationData.orderedReactionList
    +        if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
    +            holder.reactionsContainer.isVisible = false
    +        } else {
    +            holder.reactionsContainer.isVisible = true
    +            holder.reactionsContainer.removeAllViews()
    +            reactions.take(8).forEach { reaction ->
    +                val reactionButton = ReactionButton(holder.view.context)
    +                reactionButton.reactedListener = reactionClickListener
    +                reactionButton.setTag(R.id.reactionsContainer, reaction.key)
    +                reactionButton.reactionString = reaction.key
    +                reactionButton.reactionCount = reaction.count
    +                reactionButton.setChecked(reaction.addedByMe)
    +                reactionButton.isEnabled = reaction.synced
    +                holder.reactionsContainer.addView(reactionButton)
    +            }
    +            holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
    +        }
    +
    +        holder.view.setOnClickListener(baseAttributes.itemClickListener)
    +        holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
    +    }
    +
    +    override fun unbind(holder: H) {
    +        holder.readReceiptsView.unbind()
    +        super.unbind(holder)
    +    }
    +
    +    protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
    +        root.isClickable = baseAttributes.informationData.sendState.isSent()
    +        val state = if (baseAttributes.informationData.hasPendingEdits) SendState.UNSENT else baseAttributes.informationData.sendState
    +        textView?.setTextColor(baseAttributes.colorProvider.getMessageTextColor(state))
    +        failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
    +    }
    +
    +    abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
    +        val reactionsContainer by bind(R.id.reactionsContainer)
    +    }
    +
    +    /**
    +     * This class holds all the common attributes for timeline items.
    +     */
    +    interface Attributes {
    +        //            val avatarSize: Int,
    +        val informationData: MessageInformationData
    +        val avatarRenderer: AvatarRenderer
    +        val colorProvider: ColorProvider
    +        val itemLongClickListener: View.OnLongClickListener?
    +        val itemClickListener: View.OnClickListener?
    +        //        val memberClickListener: View.OnClickListener?
    +        val reactionPillCallback: TimelineEventController.ReactionPillCallback?
    +        //        val avatarCallback: TimelineEventController.AvatarCallback?
    +        val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
    +//        val emojiTypeFace: Typeface?
    +    }
    +
    +//    data class AbsAttributes(
    +//            override val informationData: MessageInformationData,
    +//            override val avatarRenderer: AvatarRenderer,
    +//            override val colorProvider: ColorProvider,
    +//            override val itemLongClickListener: View.OnLongClickListener? = null,
    +//            override val itemClickListener: View.OnClickListener? = null,
    +//            override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
    +//            override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
    +//    ) : Attributes
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    index af4c55e742..ae69164951 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
    @@ -18,22 +18,24 @@ package im.vector.riotx.features.home.room.detail.timeline.item
     
     import android.graphics.Typeface
     import android.view.View
    -import android.view.ViewGroup
     import android.widget.ImageView
     import android.widget.TextView
     import androidx.annotation.IdRes
    -import androidx.core.view.isVisible
     import com.airbnb.epoxy.EpoxyAttribute
    -import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.riotx.R
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.home.AvatarRenderer
     import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    -import im.vector.riotx.features.reactions.widget.ReactionButton
    -import im.vector.riotx.features.ui.getMessageTextColor
     
    -abstract class AbsMessageItem : BaseEventItem() {
    +/**
    + * Base timeline item that adds an optional information bar with the sender avatar, name and time
    + * Adds associated click listeners (on avatar, displayname)
    + */
    +abstract class AbsMessageItem : AbsBaseMessageItem() {
    +
    +    override val baseAttributes: AbsBaseMessageItem.Attributes
    +        get() = attributes
     
         @EpoxyAttribute
         lateinit var attributes: Attributes
    @@ -45,24 +47,6 @@ abstract class AbsMessageItem : BaseEventItem() {
             attributes.avatarCallback?.onMemberNameClicked(attributes.informationData)
         })
     
    -    private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
    -        attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
    -    })
    -
    -    var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
    -        override fun onReacted(reactionButton: ReactionButton) {
    -            attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
    -        }
    -
    -        override fun onUnReacted(reactionButton: ReactionButton) {
    -            attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false)
    -        }
    -
    -        override fun onLongClick(reactionButton: ReactionButton) {
    -            attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString)
    -        }
    -    }
    -
         override fun bind(holder: H) {
             super.bind(holder)
             if (attributes.informationData.showInformation) {
    @@ -89,60 +73,12 @@ abstract class AbsMessageItem : BaseEventItem() {
                 holder.avatarImageView.setOnLongClickListener(null)
                 holder.memberNameView.setOnLongClickListener(null)
             }
    -        holder.view.setOnClickListener(attributes.itemClickListener)
    -        holder.view.setOnLongClickListener(attributes.itemLongClickListener)
    -
    -        holder.readReceiptsView.render(
    -                attributes.informationData.readReceipts,
    -                attributes.avatarRenderer,
    -                _readReceiptsClickListener
    -        )
    -
    -        val reactions = attributes.informationData.orderedReactionList
    -        if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
    -            holder.reactionsContainer.isVisible = false
    -        } else {
    -            holder.reactionsContainer.isVisible = true
    -            holder.reactionsContainer.removeAllViews()
    -            reactions.take(8).forEach { reaction ->
    -                val reactionButton = ReactionButton(holder.view.context)
    -                reactionButton.reactedListener = reactionClickListener
    -                reactionButton.setTag(R.id.reactionsContainer, reaction.key)
    -                reactionButton.reactionString = reaction.key
    -                reactionButton.reactionCount = reaction.count
    -                reactionButton.setChecked(reaction.addedByMe)
    -                reactionButton.isEnabled = reaction.synced
    -                holder.reactionsContainer.addView(reactionButton)
    -            }
    -            holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener)
    -        }
         }
     
    -    override fun unbind(holder: H) {
    -        holder.readReceiptsView.unbind()
    -        super.unbind(holder)
    -    }
    -
    -    open fun shouldShowReactionAtBottom(): Boolean {
    -        return true
    -    }
    -
    -    override fun getEventIds(): List {
    -        return listOf(attributes.informationData.eventId)
    -    }
    -
    -    protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
    -        root.isClickable = attributes.informationData.sendState.isSent()
    -        val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState
    -        textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state))
    -        failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed()
    -    }
    -
    -    abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
    +    abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
             val avatarImageView by bind(R.id.messageAvatarImageView)
             val memberNameView by bind(R.id.messageMemberNameView)
             val timeView by bind(R.id.messageTimeView)
    -        val reactionsContainer by bind(R.id.reactionsContainer)
         }
     
         /**
    @@ -150,15 +86,15 @@ abstract class AbsMessageItem : BaseEventItem() {
          */
         data class Attributes(
                 val avatarSize: Int,
    -            val informationData: MessageInformationData,
    -            val avatarRenderer: AvatarRenderer,
    -            val colorProvider: ColorProvider,
    -            val itemLongClickListener: View.OnLongClickListener? = null,
    -            val itemClickListener: View.OnClickListener? = null,
    +            override val informationData: MessageInformationData,
    +            override val avatarRenderer: AvatarRenderer,
    +            override val colorProvider: ColorProvider,
    +            override val itemLongClickListener: View.OnLongClickListener? = null,
    +            override val itemClickListener: View.OnClickListener? = null,
                 val memberClickListener: View.OnClickListener? = null,
    -            val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
    +            override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
                 val avatarCallback: TimelineEventController.AvatarCallback? = null,
    -            val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
    +            override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
                 val emojiTypeFace: Typeface? = null
    -    )
    +    ) : AbsBaseMessageItem.Attributes
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
    index d1ceb56e3c..fe5d0d03ca 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
    @@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
     import android.os.Parcelable
     import im.vector.matrix.android.api.session.room.send.SendState
     import im.vector.matrix.android.api.util.MatrixItem
    +import im.vector.matrix.android.internal.session.room.VerificationState
     import kotlinx.android.parcel.Parcelize
     
     @Parcelize
    @@ -27,6 +28,7 @@ data class MessageInformationData(
             val senderId: String,
             val sendState: SendState,
             val time: CharSequence? = null,
    +        val ageLocalTS : Long?,
             val avatarUrl: String?,
             val memberName: CharSequence? = null,
             val showInformation: Boolean = true,
    @@ -34,13 +36,20 @@ data class MessageInformationData(
             val orderedReactionList: List? = null,
             val hasBeenEdited: Boolean = false,
             val hasPendingEdits: Boolean = false,
    -        val readReceipts: List = emptyList()
    +        val readReceipts: List = emptyList(),
    +        val referencesInfoData: ReferencesInfoData? = null,
    +        val sentByMe : Boolean
     ) : Parcelable {
     
         val matrixItem: MatrixItem
             get() = MatrixItem.UserItem(senderId, memberName?.toString(), avatarUrl)
     }
     
    +@Parcelize
    +data class ReferencesInfoData(
    +        val verificationStatus: VerificationState
    +) : Parcelable
    +
     @Parcelize
     data class ReactionInfoData(
             val key: String,
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt
    new file mode 100644
    index 0000000000..036bf2b036
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt
    @@ -0,0 +1,90 @@
    +/*
    + * 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.home.room.detail.timeline.item
    +
    +import android.annotation.SuppressLint
    +import android.graphics.Typeface
    +import android.view.View
    +import android.widget.ImageView
    +import android.widget.RelativeLayout
    +import androidx.appcompat.widget.AppCompatTextView
    +import androidx.core.content.ContextCompat
    +import androidx.core.view.updateLayoutParams
    +import com.airbnb.epoxy.EpoxyAttribute
    +import com.airbnb.epoxy.EpoxyModelClass
    +import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    +
    +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
    +abstract class VerificationRequestConclusionItem : AbsBaseMessageItem() {
    +
    +    override val baseAttributes: AbsBaseMessageItem.Attributes
    +        get() = attributes
    +
    +    @EpoxyAttribute
    +    lateinit var attributes: Attributes
    +
    +    override fun getViewType() = STUB_ID
    +
    +    @SuppressLint("SetTextI18n")
    +    override fun bind(holder: Holder) {
    +        super.bind(holder)
    +        holder.endGuideline.updateLayoutParams {
    +            this.marginEnd = leftGuideline
    +        }
    +        val title = if (attributes.isPositive) R.string.sas_verified else R.string.verification_conclusion_warning
    +        holder.titleView.text = holder.view.context.getString(title)
    +        holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
    +
    +        val startDrawable = if (attributes.isPositive) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
    +        holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
    +                ContextCompat.getDrawable(holder.view.context, startDrawable),
    +                null, null, null
    +        )
    +
    +        renderSendState(holder.view, null, holder.failedToSendIndicator)
    +    }
    +
    +    class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
    +        val titleView by bind(R.id.itemVerificationDoneTitleTextView)
    +        val descriptionView by bind(R.id.itemVerificationDoneDetailTextView)
    +        val endGuideline by bind(R.id.messageEndGuideline)
    +        val failedToSendIndicator by bind(R.id.messageFailToSendIndicator)
    +    }
    +
    +    companion object {
    +        private const val STUB_ID = R.id.messageVerificationDoneStub
    +    }
    +
    +    /**
    +     * This class holds all the common attributes for timeline items.
    +     */
    +    data class Attributes(
    +            val toUserId: String,
    +            val toUserName: String,
    +            val isPositive: Boolean,
    +            override val informationData: MessageInformationData,
    +            override val avatarRenderer: AvatarRenderer,
    +            override val colorProvider: ColorProvider,
    +            override val itemLongClickListener: View.OnLongClickListener? = null,
    +            override val itemClickListener: View.OnClickListener? = null,
    +            override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
    +            override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
    +            val emojiTypeFace: Typeface? = null
    +    ) : AbsBaseMessageItem.Attributes
    +}
    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
    new file mode 100644
    index 0000000000..24f992a001
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
    @@ -0,0 +1,186 @@
    +/*
    + * 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.home.room.detail.timeline.item
    +
    +import android.annotation.SuppressLint
    +import android.graphics.Typeface
    +import android.view.View
    +import android.view.ViewGroup
    +import android.widget.Button
    +import android.widget.ImageView
    +import android.widget.RelativeLayout
    +import android.widget.TextView
    +import androidx.appcompat.widget.AppCompatTextView
    +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.VerificationService
    +import im.vector.matrix.android.internal.session.room.VerificationState
    +import im.vector.riotx.R
    +import im.vector.riotx.core.resources.ColorProvider
    +import im.vector.riotx.core.utils.DebouncedClickListener
    +import im.vector.riotx.features.home.AvatarRenderer
    +import im.vector.riotx.features.home.room.detail.RoomDetailAction
    +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
    +
    +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
    +abstract class VerificationRequestItem : AbsBaseMessageItem() {
    +
    +    override val baseAttributes: AbsBaseMessageItem.Attributes
    +        get() = attributes
    +
    +    @EpoxyAttribute
    +    lateinit var attributes: Attributes
    +
    +    @EpoxyAttribute
    +    var callback: TimelineEventController.Callback? = null
    +
    +    override fun getViewType() = STUB_ID
    +
    +    @SuppressLint("SetTextI18n")
    +    override fun bind(holder: Holder) {
    +        super.bind(holder)
    +
    +        holder.endGuideline.updateLayoutParams {
    +            this.marginEnd = leftGuideline
    +        }
    +
    +        holder.titleView.text = if (attributes.informationData.sentByMe) {
    +            holder.view.context.getString(R.string.verification_sent)
    +        } else {
    +            holder.view.context.getString(R.string.verification_request)
    +        }
    +
    +        holder.descriptionView.text = if (!attributes.informationData.sentByMe) {
    +            "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
    +        } else {
    +            "${attributes.otherUserName} (${attributes.otherUserId})"
    +        }
    +
    +        when (attributes.informationData.referencesInfoData?.verificationStatus) {
    +            VerificationState.REQUEST,
    +            null                                -> {
    +                holder.buttonBar.isVisible = !attributes.informationData.sentByMe
    +                holder.statusTextView.text = null
    +                holder.statusTextView.isVisible = false
    +            }
    +            VerificationState.CANCELED_BY_OTHER -> {
    +                holder.buttonBar.isVisible = false
    +                holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
    +                holder.statusTextView.isVisible = true
    +            }
    +            VerificationState.CANCELED_BY_ME    -> {
    +                holder.buttonBar.isVisible = false
    +                holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_you_cancelled)
    +                holder.statusTextView.isVisible = true
    +            }
    +            VerificationState.WAITING           -> {
    +                holder.buttonBar.isVisible = false
    +                holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_waiting)
    +                holder.statusTextView.isVisible = true
    +            }
    +            VerificationState.DONE              -> {
    +                holder.buttonBar.isVisible = false
    +                holder.statusTextView.text = if (attributes.informationData.sentByMe) {
    +                    holder.view.context.getString(R.string.verification_request_other_accepted, attributes.otherUserName)
    +                } else {
    +                    holder.view.context.getString(R.string.verification_request_you_accepted)
    +                }
    +                holder.statusTextView.isVisible = true
    +            }
    +            else                                -> {
    +                holder.buttonBar.isVisible = false
    +                holder.statusTextView.text = null
    +                holder.statusTextView.isVisible = false
    +            }
    +        }
    +
    +        // Always hide buttons if request is too old
    +        if (!VerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {
    +            holder.buttonBar.isVisible = false
    +        }
    +
    +        holder.callback = callback
    +        holder.attributes = attributes
    +
    +        renderSendState(holder.view, null, holder.failedToSendIndicator)
    +    }
    +
    +    override fun unbind(holder: Holder) {
    +        super.unbind(holder)
    +        holder.callback = null
    +        holder.attributes = null
    +    }
    +
    +    class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
    +
    +        var callback: TimelineEventController.Callback? = null
    +        var attributes: Attributes? = null
    +
    +        private val _clickListener = DebouncedClickListener(View.OnClickListener {
    +            val att = attributes ?: return@OnClickListener
    +            if (it == acceptButton) {
    +                callback?.onTimelineItemAction(RoomDetailAction.AcceptVerificationRequest(
    +                        att.referenceId,
    +                        att.otherUserId,
    +                        att.fromDevide))
    +            } else if (it == declineButton) {
    +                callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId, att.otherUserId, att.fromDevide))
    +            }
    +        })
    +
    +        val titleView by bind(R.id.itemVerificationTitleTextView)
    +        val descriptionView by bind(R.id.itemVerificationDetailTextView)
    +        val buttonBar by bind(R.id.itemVerificationButtonBar)
    +        val statusTextView by bind(R.id.itemVerificationStatusText)
    +        val endGuideline by bind(R.id.messageEndGuideline)
    +        private val declineButton by bind