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 3c77661b8b..81096424d5 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 @@ -1161,7 +1161,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup2.isEnabled) // - Validate the old device from the new one - aliceSession2.setDeviceVerification(DeviceTrustLevel(false, true), 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/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/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 de657e6abb..2b1e92c317 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 @@ -59,7 +59,7 @@ interface CryptoService { fun setWarnOnUnknownDevices(warn: Boolean) - fun setDeviceVerification(trustLevel: DeviceTrustLevel, deviceId: String, userId: String) + fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) fun getUserDevices(userId: String): MutableList 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 index a626dd5573..a5230fb995 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.crypto.verification.PendingVerification * SAS verification is intended to be a highly interactive process for users, * and as such exposes verification methods which are easier for users to use. */ +// TODO Rename to VerificationService and reorganize packages? interface SasVerificationService { fun addListener(listener: SasVerificationListener) @@ -69,6 +70,7 @@ interface SasVerificationService { // fun transactionUpdated(tx: SasVerificationTransaction) + // TODO Rename to VerificationListener interface SasVerificationListener { fun transactionCreated(tx: SasVerificationTransaction) fun transactionUpdated(tx: SasVerificationTransaction) 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 e4e0baa3ac..640bc501e9 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 @@ -50,5 +50,8 @@ interface SasVerificationTransaction { fun shortCodeDoesNotMatch() - fun isToDeviceTransport() : Boolean + fun isToDeviceTransport(): Boolean + + // TODO Not sure this is the right place to add this, because it is not Sas + fun userHasScannedRemoteQrCode(scannedData: String) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt index a2dd90bc84..b8f0f23891 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationMethod.kt @@ -17,10 +17,13 @@ package im.vector.matrix.android.api.session.crypto.sas /** - * Verification methods supported (or to be supported) by the matrix SDK + * Verification methods */ enum class VerificationMethod { + // Use it when your application supports the SAS verification method SAS, - // Not supported yet - SCAN + // Use it if your application is able to display QR codes + QR_CODE_SHOW, + // Use it if your application is able to scan QR codes + QR_CODE_SCAN } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index ecd666948d..ffc8ba69d8 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 @@ -438,12 +438,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(trustLevel: DeviceTrustLevel, deviceId: String, userId: String) { - setDeviceVerificationAction.handle(trustLevel, deviceId, userId) + override fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { + setDeviceVerificationAction.handle(trustLevel, userId, deviceId) } /** 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 6d5c6687d4..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 @@ -28,7 +28,7 @@ internal class SetDeviceVerificationAction @Inject constructor( @UserId private val userId: String, private val keysBackup: KeysBackup) { - fun handle(trustLevel: DeviceTrustLevel, deviceId: String, userId: String) { + fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) { val device = cryptoStore.getUserDevice(userId, deviceId) // Sanity check 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 index 325f3b7563..d95c557948 100644 --- 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.crosssigning -import android.util.Base64 import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials @@ -83,40 +82,43 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.i("## CrossSigning - Found Existing self signed keys") Timber.i("## CrossSigning - Checking if private keys are known") - cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeyinfo -> - privateKeyinfo.master?.let { privateKey -> - val keySeed = Base64.decode(privateKey, Base64.NO_PADDING) - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(keySeed) == 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 - } - } - privateKeyinfo.user?.let { privateKey -> - val keySeed = Base64.decode(privateKey, Base64.NO_PADDING) - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(keySeed) == 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 - } - } - privateKeyinfo.selfSigned?.let { privateKey -> - val keySeed = Base64.decode(privateKey, Base64.NO_PADDING) - val pkSigning = OlmPkSigning() - if (pkSigning.initWithSeed(keySeed) == 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 - } - } + 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 + } + } } } } catch (e: Throwable) { @@ -369,7 +371,9 @@ internal class DefaultCrossSigningService @Inject constructor( // Is the master key trusted // 1) check if I know the private key - val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()?.master + val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() + ?.master + ?.fromBase64NoPadding() var isMaterKeyTrusted = false if (masterPrivateKey != null) { @@ -377,11 +381,12 @@ internal class DefaultCrossSigningService @Inject constructor( var olmPkSigning: OlmPkSigning? = null try { olmPkSigning = OlmPkSigning() - val expectedPK = olmPkSigning.initWithSeed(Base64.decode(masterPrivateKey, Base64.NO_PADDING)) + val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey) isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK } catch (failure: Throwable) { - olmPkSigning?.releaseSigning() + Timber.e(failure) } + olmPkSigning?.releaseSigning() } else { // Maybe it's signed by a locally trusted device? myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) -> 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 index 3d0f68d4f6..6ffc341881 100644 --- 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 @@ -28,6 +28,10 @@ fun CryptoCrossSigningKey.canonicalSignable(): String { return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) } -fun ByteArray.toBase64NoPadding() : String? { - return Base64.encodeToString(this, Base64.NO_PADDING) +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/model/rest/VerificationMethodValues.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt index 168a8c8f48..e3659bbde2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/VerificationMethodValues.kt @@ -19,14 +19,25 @@ package im.vector.matrix.android.internal.crypto.model.rest import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod internal const val VERIFICATION_METHOD_SAS = "m.sas.v1" -internal const val VERIFICATION_METHOD_SCAN = "m.qr_code.scan.v1" + +// Qr code +// Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#verification-methods +internal const val VERIFICATION_METHOD_QR_CODE_SHOW = "m.qr_code.show.v1" +internal const val VERIFICATION_METHOD_QR_CODE_SCAN = "m.qr_code.scan.v1" +internal const val VERIFICATION_METHOD_RECIPROCATE = "m.reciprocate.v1" internal fun VerificationMethod.toValue(): String { return when (this) { - VerificationMethod.SAS -> VERIFICATION_METHOD_SAS - VerificationMethod.SCAN -> VERIFICATION_METHOD_SCAN + VerificationMethod.SAS -> VERIFICATION_METHOD_SAS + VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW } } -// TODO Add SCAN -internal val supportedVerificationMethods = listOf(VERIFICATION_METHOD_SAS) +internal val supportedVerificationMethods = + listOf( + VERIFICATION_METHOD_SAS, + VERIFICATION_METHOD_QR_CODE_SHOW, + VERIFICATION_METHOD_QR_CODE_SCAN, + VERIFICATION_METHOD_RECIPROCATE + ) 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 index e2add0beff..a10b6d2645 100644 --- 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 @@ -1,3 +1,19 @@ +/* + * 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( 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 index faa21f9356..d8a98ff280 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -41,6 +41,7 @@ import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import okhttp3.internal.toImmutableList import timber.log.Timber import java.util.* import javax.inject.Inject @@ -204,8 +205,8 @@ internal class DefaultSasVerificationService @Inject constructor( override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), - deviceID, - userId) + userId, + deviceID) listeners.forEach { try { @@ -726,10 +727,11 @@ internal class DefaultSasVerificationService @Inject constructor( val transport = sasTransportRoomMessageFactory.createTransport(roomId, null) // Cancel existing pending requests? - requestsForUser.forEach { existingRequest -> + requestsForUser.toImmutableList().forEach { existingRequest -> existingRequest.transactionId?.let { tid -> if (!existingRequest.isFinished) { Timber.d("## SAS, cancelling pending requests to start a new one") + updatePendingRequest(existingRequest.copy(cancelConclusion = CancelCode.User)) transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) } } @@ -819,6 +821,7 @@ internal class DefaultSasVerificationService @Inject constructor( if (existingRequest != null) { // we need to send a ready event, with matching methods val transport = sasTransportRoomMessageFactory.createTransport(roomId, null) + // TODO We should not use supportedVerificationMethods here, because it depends on the client implementation val methods = existingRequest.requestInfo?.methods?.intersect(supportedVerificationMethods)?.toList() if (methods.isNullOrEmpty()) { Timber.i("Cannot ready this request, no common methods found txId:$transactionId") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 0ac3847a53..531d2c8fa1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -18,9 +18,10 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SCAN -import java.util.* +import java.util.UUID /** * Stores current pending verification requests @@ -46,8 +47,9 @@ data class PendingVerificationRequest( fun hasMethod(method: VerificationMethod): Boolean? { return when (method) { - VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS) - VerificationMethod.SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SCAN) + VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS) + VerificationMethod.QR_CODE_SHOW -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW) + VerificationMethod.QR_CODE_SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index c0dfc73129..682c54f45f 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/SASVerificationTransaction.kt @@ -204,7 +204,7 @@ internal abstract class SASVerificationTransaction( cancel(CancelCode.MismatchedSas) } - override fun isToDeviceTransport() : Boolean { + override fun isToDeviceTransport(): Boolean { return transport is SasTransportToDevice } @@ -228,6 +228,10 @@ internal abstract class SASVerificationTransaction( abstract fun onKeyVerificationMac(vKey: VerificationInfoMac) + override fun userHasScannedRemoteQrCode(scannedData: String) { + // TODO + } + protected fun verifyMacs() { Timber.v("## SAS verifying macs for id:$transactionId") state = SasVerificationTxState.Verifying @@ -328,17 +332,17 @@ internal abstract class SASVerificationTransaction( // TODO what if the otherDevice is not in this list? and should we verifiedDevices.forEach { - setDeviceVerified(it, otherUserId) + setDeviceVerified(otherUserId, it) } transport.done(transactionId) state = SasVerificationTxState.Verified } - private fun setDeviceVerified(deviceId: String, userId: String) { + private fun setDeviceVerified(userId: String, deviceId: String) { // TODO should not override cross sign status setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), - deviceId, - userId) + userId, + deviceId) } override fun cancel() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt index 12fe5c338f..2e6d5ed32a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt @@ -37,8 +37,8 @@ internal interface SasTransport { fun sendVerificationRequest(supportedMethods: List, localID: String, otherUserId: String, - roomId: String, callback: - (String?, MessageVerificationRequestContent?) -> Unit) + roomId: String, + callback: (String?, MessageVerificationRequestContent?) -> Unit) fun cancelTransaction(transactionId: String, otherUserId: String, 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..2f90d36140 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt @@ -0,0 +1,109 @@ +/* + * 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 + +/** + * Generate an URL to generate a QR code of the form: + *
+ * https://matrix.to/#/?
+ *     request=
+ *     &action=verify
+ *     &key_=...
+ *     &secret=
+ *     &other_user_key=
+ * 
+ */ +fun QrCodeData.toUrl(): String { + return buildString { + append(PermalinkFactory.createPermalink(userId)) + append("?request=") + append(PermalinkFactory.escape(requestEventId)) + append("&action=") + append(PermalinkFactory.escape(action)) + + for ((keyId, key) in keys) { + append("&key_") + append(PermalinkFactory.escape(keyId)) + append("=") + append(PermalinkFactory.escape(key)) + } + + append("&secret=") + append(PermalinkFactory.escape(sharedSecret)) + append("&other_user_key=") + append(PermalinkFactory.escape(otherUserKey)) + } +} + +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 -> PermalinkFactory.unescape(value) }) + }.toMap() + + val action = keyValues["action"] ?: return null + + val requestEventId = keyValues["request"]?.takeIf { MatrixPatterns.isEventId(it) } ?: return null + val sharedSecret = keyValues["secret"] ?: return null + val otherUserKey = keyValues["other_user_key"] ?: return null + + val keys = keyValues.keys + .filter { it.startsWith("key_") } + .map { + it.substringAfter("key_") to (keyValues[it] ?: return null) + } + .toMap() + + return QrCodeData( + userId, + requestEventId, + action, + keys, + sharedSecret, + otherUserKey + ) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt new file mode 100644 index 0000000000..8b400413b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.verification.qrcode + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format + */ +data class QrCodeData( + val userId: String, + // the event ID of the associated verification request event. + val requestEventId: String, + // The action + val action: String, + // key_: each key that the user wants verified will have an entry of this form, where the value is the key in unpadded base64. + // The QR code should contain at least the user's master cross-signing key. + val keys: Map, + // random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded). + val sharedSecret: String, + // the other user's master cross-signing key, in unpadded base64. In other words, if Alice is displaying the QR code, + // this would be the copy of Bob's master cross-signing key that Alice has. + val otherUserKey: String +) { + companion object { + const val ACTION_VERIFY = "verify" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt new file mode 100644 index 0000000000..d319ebd88c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/SharedSecret.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.verification.qrcode + +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import java.security.SecureRandom + +fun generateSharedSecret(): String { + val secureRandom = SecureRandom() + + // 256 bits long + val secretBytes = ByteArray(32) + secureRandom.nextBytes(secretBytes) + return secretBytes.toBase64NoPadding() +} diff --git a/matrix-sdk-android/src/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..c356ee4795 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.verification.qrcode + +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldNotBeNull +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@Suppress("SpellCheckingInspection") +@FixMethodOrder(MethodSorters.JVM) +class QrCodeTest { + + private val basicQrCodeData = QrCodeData( + userId = "@benoit:matrix.org", + requestEventId = "\$azertyazerty", + action = QrCodeData.ACTION_VERIFY, + keys = mapOf( + "1" to "abcdef", + "2" to "ghijql" + ), + sharedSecret = "sharedSecret", + otherUserKey = "otherUserKey" + ) + + private val basicUrl = "https://matrix.to/#/@benoit:matrix.org?request=\$azertyazerty&action=verify&key_1=abcdef&key_2=ghijql&secret=sharedSecret&other_user_key=otherUserKey" + + @Test + fun testNominalCase() { + val url = basicQrCodeData.toUrl() + + url shouldBeEqualTo basicUrl + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit:matrix.org" + decodedData.requestEventId shouldBeEqualTo "\$azertyazerty" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey shouldBeEqualTo "otherUserKey" + } + + @Test + fun testSlashCase() { + val url = basicQrCodeData + .copy( + userId = "@benoit/foo:matrix.org", + requestEventId = "\$azertyazerty/bar" + ) + .toUrl() + + url shouldBeEqualTo basicUrl + .replace("@benoit", "@benoit%2Ffoo") + .replace("azertyazerty", "azertyazerty%2Fbar") + + val decodedData = url.toQrCodeData() + + decodedData.shouldNotBeNull() + + decodedData.userId shouldBeEqualTo "@benoit/foo:matrix.org" + decodedData.requestEventId shouldBeEqualTo "\$azertyazerty/bar" + decodedData.keys["1"]?.shouldBeEqualTo("abcdef") + decodedData.keys["2"]?.shouldBeEqualTo("ghijql") + decodedData.sharedSecret shouldBeEqualTo "sharedSecret" + decodedData.otherUserKey shouldBeEqualTo "otherUserKey" + } + + @Test + fun testMissingActionCase() { + basicUrl.replace("&action=verify", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testOtherActionCase() { + basicUrl.replace("&action=verify", "&action=confirm") + .toQrCodeData() + ?.action + ?.shouldBeEqualTo("confirm") + } + + @Test + fun testBadRequestEventId() { + basicUrl.replace("\$azertyazerty", "@azertyazerty") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingUserId() { + basicUrl.replace("@benoit:matrix.org", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testBadUserId() { + basicUrl.replace("@benoit:matrix.org", "@benoit") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingSecret() { + basicUrl.replace("&secret=sharedSecret", "") + .toQrCodeData() + .shouldBeNull() + } + + @Test + fun testMissingOtherUserKey() { + basicUrl.replace("&other_user_key=otherUserKey", "") + .toQrCodeData() + .shouldBeNull() + } +} diff --git a/vector/build.gradle b/vector/build.gradle index 14ec9f2c21..3bcb4a35b9 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -27,7 +27,7 @@ static def generateVersionCodeFromTimestamp() { // It's unix timestamp, minus timestamp of October 3rd 2018 (first commit date) divided by 100: It's incremented by one every 100 seconds. // plus 20_000_000 for compatibility reason with the previous way the Version Code was computed // Note that the result will be multiplied by 10 when adding the digit for the arch - return ((getGitTimestamp() - 1_538_524_800 ) / 100).toInteger() + 20_000_000 + return ((getGitTimestamp() - 1_538_524_800) / 100).toInteger() + 20_000_000 } def generateVersionCodeFromVersionName() { @@ -351,6 +351,10 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" + // QR-code + implementation 'com.google.zxing:core:3.4.0' + implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' diff --git a/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotx/features/debug/DebugMenuActivity.kt index db3c91d441..96a6f4bb2d 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 @@ -37,7 +38,15 @@ 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.qrcode.toQrCode +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() { @@ -51,6 +60,15 @@ class DebugMenuActivity : VectorBaseActivity() { injector.inject(this) } + override fun initUiAndData() { + renderQrCode("https://www.example.org") + } + + private fun renderQrCode(text: String) { + val qrBitmap = text.toQrCode(200, 200) + debug_qr_code.setImageBitmap(qrBitmap) + } + @OnClick(R.id.debug_test_text_view_link) fun testTextViewLink() { startActivity(Intent(this, TestLinkifyActivity::class.java)) @@ -214,4 +232,37 @@ class DebugMenuActivity : VectorBaseActivity() { } }) } + + @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 5d18121f5c..52b993e223 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -68,6 +68,19 @@ android:layout_height="wrap_content" android:text="Initialize XSigning" /> + + + + diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c6e4b51c44..3207ab257a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + - + + Copyright 2017 Sergiy Kovalchuk +
  • + ZXing +
    + Copyright 2007 ZXing authors +
  • +
  • + barcodescanner +
    + Copyright (c) 2014 Dushyanth Maguluru +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt
    index 53bd7d169f..52c8b840e3 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
    @@ -22,32 +22,50 @@ import androidx.fragment.app.FragmentFactory
     import dagger.Binds
     import dagger.Module
     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.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.request.VerificationRequestFragment
    +import im.vector.riotx.features.grouplist.GroupListFragment
     import im.vector.riotx.features.home.HomeDetailFragment
     import im.vector.riotx.features.home.HomeDrawerFragment
     import im.vector.riotx.features.home.LoadingFragment
    -import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
    -import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
    -import im.vector.riotx.features.grouplist.GroupListFragment
     import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
     import im.vector.riotx.features.home.room.detail.RoomDetailFragment
     import im.vector.riotx.features.home.room.list.RoomListFragment
    -import im.vector.riotx.features.login.*
    +import im.vector.riotx.features.login.LoginCaptchaFragment
    +import im.vector.riotx.features.login.LoginFragment
    +import im.vector.riotx.features.login.LoginGenericTextInputFormFragment
    +import im.vector.riotx.features.login.LoginResetPasswordFragment
    +import im.vector.riotx.features.login.LoginResetPasswordMailConfirmationFragment
    +import im.vector.riotx.features.login.LoginResetPasswordSuccessFragment
    +import im.vector.riotx.features.login.LoginServerSelectionFragment
    +import im.vector.riotx.features.login.LoginServerUrlFormFragment
    +import im.vector.riotx.features.login.LoginSignUpSignInSelectionFragment
    +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.roommemberprofile.RoomMemberProfileFragment
    +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
     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.roomprofile.RoomProfileFragment
     import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
    -import im.vector.riotx.features.settings.*
    +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.devices.VectorSettingsDevicesFragment
     import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
     import im.vector.riotx.features.settings.push.PushGatewaysFragment
    @@ -296,4 +314,9 @@ interface FragmentModule {
         @IntoMap
         @FragmentKey(VerificationConclusionFragment::class)
         fun bindVerificationConclusionFragment(fragment: VerificationConclusionFragment): Fragment
    +
    +    @Binds
    +    @IntoMap
    +    @FragmentKey(QrCodeScannerFragment::class)
    +    fun bindQrCodeScannerFragment(fragment: QrCodeScannerFragment): 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 85e761e0b9..92e5a89ddb 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,12 +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
    @@ -44,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
    @@ -141,6 +142,8 @@ interface ScreenComponent {
     
         fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
     
    +    fun inject(activity: QrCodeScannerActivity)
    +
         fun inject(activity: DebugMenuActivity)
     
         fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet)
    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..0af2cf6777
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/qrcode/QrCode.kt
    @@ -0,0 +1,50 @@
    +/*
    + * 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.toQrCode(width: Int,
    +                    height: Int,
    +                    @ColorInt backgroundColor: Int = Color.WHITE,
    +                    @ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
    +    return QRCodeWriter().encode(
    +            this,
    +            BarcodeFormat.QR_CODE,
    +            width,
    +            height
    +    ).toBitmap(backgroundColor, foregroundColor)
    +}
    +
    +fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
    +                       @ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
    +    val height: Int = height
    +    val width: Int = width
    +    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/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt
    index 0f4b651d43..28155e2d59 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
    @@ -89,7 +89,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)
    @@ -110,7 +110,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
                     }
     
                     if (deviceInfo.isUnknown) {
    -                    session?.setDeviceVerification(DeviceTrustLevel(false, false), deviceId, userId)
    +                    session?.setDeviceVerification(DeviceTrustLevel(false, false), userId, deviceId)
     
                         deviceInfo.trustLevel = DeviceTrustLevel(false, false)
     
    @@ -181,7 +181,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
    @@ -189,7 +189,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
     
             alert.colorRes = R.color.key_share_req_accent_color
     
    -        val mappingKey = keyForMap(deviceId, userId)
    +        val mappingKey = keyForMap(userId, deviceId)
             alert.dismissedAction = Runnable {
                 denyAllRequests(mappingKey)
             }
    @@ -249,7 +249,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,7 +257,7 @@ 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))
             }
         }
     
    @@ -275,11 +275,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
     
         override fun markedAsManuallyVerified(userId: String, deviceId: String) {
             // accept related requests
    -        shareAllSessions(keyForMap(deviceId, userId))
    -        PopupAlertManager.cancelAlert(alertManagerId(deviceId, userId))
    +        shareAllSessions(keyForMap(userId, deviceId))
    +        PopupAlertManager.cancelAlert(alertManagerId(userId, deviceId))
         }
     
    -    private fun keyForMap(deviceId: String, userId: String) = "$deviceId$userId"
    +    private fun keyForMap(userId: String, deviceId: String) = "$deviceId$userId"
     
    -    private fun alertManagerId(deviceId: String, userId: String) = "ikr_$deviceId$userId"
    +    private fun alertManagerId(userId: String, deviceId: String) = "ikr_$deviceId$userId"
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    index 0bc9a3e144..fae7037403 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/Config.kt
    @@ -18,5 +18,12 @@ package im.vector.riotx.features.crypto.verification
     
     import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
     
    -// TODO Add support for SCAN (QR code)
    -val supportedVerificationMethods = listOf(VerificationMethod.SAS)
    +val supportedVerificationMethods =
    +        listOf(
    +                // RiotX supports SAS verification
    +                VerificationMethod.SAS,
    +                // RiotX is able to show QR codes
    +                VerificationMethod.QR_CODE_SHOW,
    +                // RiotX is able to scan QR codes
    +                VerificationMethod.QR_CODE_SCAN
    +        )
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    new file mode 100644
    index 0000000000..de3ea98df5
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
    @@ -0,0 +1,28 @@
    +/*
    + * Copyright 2020 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.riotx.features.crypto.verification
    +
    +import im.vector.riotx.core.platform.VectorViewModelAction
    +
    +sealed class VerificationAction : VectorViewModelAction {
    +    data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
    +    data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
    +    data class RemoteQrCodeScanned(val userID: String, val sasTransactionId: String, val scannedData: String) : VerificationAction()
    +    data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    +    data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    +    object GotItConclusion : VerificationAction()
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    index 14c32efc27..bd7c280295 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
    @@ -131,7 +131,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
                         showFragment(VerificationEmojiCodeFragment::class, Bundle().apply {
                             putParcelable(MvRx.KEY_ARG, VerificationArgs(
                                     it.otherUserMxItem?.id ?: "",
    -                                it.transactionId))
    +                                // If it was outgoing it.transaction id would be null, but the pending request
    +                                // would be updated (from localID to txId)
    +                                it.pendingRequest?.transactionId ?: it.transactionId))
                         })
                     }
                     SasVerificationTxState.Verified,
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    index 4008207b62..5f8c33f610 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
    @@ -17,17 +17,25 @@ package im.vector.riotx.features.crypto.verification
     
     import androidx.lifecycle.LiveData
     import androidx.lifecycle.MutableLiveData
    -import com.airbnb.mvrx.*
    +import com.airbnb.mvrx.Async
    +import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.MvRxState
    +import com.airbnb.mvrx.MvRxViewModelFactory
    +import com.airbnb.mvrx.Success
    +import com.airbnb.mvrx.ViewModelContext
     import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.matrix.android.api.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.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.VerificationMethod
     import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.util.toMatrixItem
     import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
     import im.vector.riotx.core.di.HasScreenInjector
     import im.vector.riotx.core.platform.VectorViewModel
    -import im.vector.riotx.core.platform.VectorViewModelAction
     import im.vector.riotx.core.utils.LiveEvent
     
     data class VerificationBottomSheetViewState(
    @@ -39,14 +47,6 @@ data class VerificationBottomSheetViewState(
             val cancelCode: CancelCode? = null
     ) : MvRxState
     
    -sealed class VerificationAction : VectorViewModelAction {
    -    data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
    -    data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
    -    data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    -    data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
    -    object GotItConclusion : VerificationAction()
    -}
    -
     class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
                                                                        private val session: Session)
         : VectorViewModel(initialState),
    @@ -122,6 +122,12 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
                             callback = null
                     )
                 }
    +            is VerificationAction.RemoteQrCodeScanned     -> {
    +                // TODO Use session.getCrossSigningService()?
    +                session.getSasVerificationService()
    +                        .getExistingTransaction(action.userID, action.sasTransactionId)
    +                        ?.userHasScannedRemoteQrCode(action.scannedData)
    +            }
                 is VerificationAction.SASMatchAction          -> {
                     session.getSasVerificationService()
                             .getExistingTransaction(action.userID, action.sasTransactionId)
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    index 8760a8603e..0bf38979da 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodController.kt
    @@ -19,8 +19,10 @@ 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.qrcode.toQrCode
     import im.vector.riotx.core.resources.ColorProvider
     import im.vector.riotx.core.resources.StringProvider
    +import im.vector.riotx.core.utils.DimensionConverter
     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
    @@ -28,7 +30,8 @@ import javax.inject.Inject
     
     class VerificationChooseMethodController @Inject constructor(
             private val stringProvider: StringProvider,
    -        private val colorProvider: ColorProvider
    +        private val colorProvider: ColorProvider,
    +        private val dimensionConverter: DimensionConverter
     ) : EpoxyController() {
     
         var listener: Listener? = null
    @@ -43,33 +46,40 @@ class VerificationChooseMethodController @Inject constructor(
         override fun buildModels() {
             val state = viewState ?: return
     
    -        if (state.QRModeAvailable) {
    +        if (state.otherCanScanQrCode || state.otherCanShowQrCode) {
                 bottomSheetVerificationNoticeItem {
                     id("notice")
                     notice(stringProvider.getString(R.string.verification_scan_notice))
                 }
     
    -            // TODO Generate the QR code
    -            bottomSheetVerificationBigImageItem {
    -                id("qr")
    -                imageRes(R.drawable.riotx_logo)
    +            if (state.otherCanScanQrCode && !state.QRtext.isNullOrBlank()) {
    +                // Generate the QR code
    +                val size = dimensionConverter.dpToPx(180)
    +                val qrCodeBitmap = state.QRtext.toQrCode(size, size)
    +
    +                bottomSheetVerificationBigImageItem {
    +                    id("qr")
    +                    imageBitmap(qrCodeBitmap)
    +                }
    +
    +                dividerItem {
    +                    id("sep0")
    +                }
                 }
     
    -            dividerItem {
    -                id("sep0")
    -            }
    +            if (state.otherCanShowQrCode) {
    +                bottomSheetVerificationActionItem {
    +                    id("openCamera")
    +                    title(stringProvider.getString(R.string.verification_scan_their_code))
    +                    titleColor(colorProvider.getColor(R.color.riotx_accent))
    +                    iconRes(R.drawable.ic_camera)
    +                    iconColor(colorProvider.getColor(R.color.riotx_accent))
    +                    listener { listener?.openCamera() }
    +                }
     
    -            bottomSheetVerificationActionItem {
    -                id("openCamera")
    -                title(stringProvider.getString(R.string.verification_scan_their_code))
    -                titleColor(colorProvider.getColor(R.color.riotx_accent))
    -                iconRes(R.drawable.ic_camera)
    -                iconColor(colorProvider.getColor(R.color.riotx_accent))
    -                listener { listener?.openCamera() }
    -            }
    -
    -            dividerItem {
    -                id("sep1")
    +                dividerItem {
    +                    id("sep1")
    +                }
                 }
     
                 bottomSheetVerificationActionItem {
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    index b782afca39..110047b49c 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodFragment.kt
    @@ -15,6 +15,8 @@
      */
     package im.vector.riotx.features.crypto.verification.choose
     
    +import android.app.Activity
    +import android.content.Intent
     import android.os.Bundle
     import android.view.View
     import com.airbnb.mvrx.fragmentViewModel
    @@ -24,9 +26,15 @@ import im.vector.riotx.R
     import im.vector.riotx.core.extensions.cleanup
     import im.vector.riotx.core.extensions.configureWith
     import im.vector.riotx.core.platform.VectorBaseFragment
    +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
    +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
    +import im.vector.riotx.core.utils.allGranted
    +import im.vector.riotx.core.utils.checkPermissions
     import im.vector.riotx.features.crypto.verification.VerificationAction
     import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewModel
    +import im.vector.riotx.features.qrcode.QrCodeScannerActivity
     import kotlinx.android.synthetic.main.bottom_sheet_verification_child_fragment.*
    +import timber.log.Timber
     import javax.inject.Inject
     
     class VerificationChooseMethodFragment @Inject constructor(
    @@ -68,6 +76,47 @@ class VerificationChooseMethodFragment @Inject constructor(
         }
     
         override fun openCamera() {
    -        // TODO
    +        if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
    +            doOpenQRCodeScanner()
    +        }
    +    }
    +
    +    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    +        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    +
    +        if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA && allGranted(grantResults)) {
    +            doOpenQRCodeScanner()
    +        }
    +    }
    +
    +    private fun doOpenQRCodeScanner() {
    +        QrCodeScannerActivity.startForResult(this)
    +    }
    +
    +    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    +        super.onActivityResult(requestCode, resultCode, data)
    +
    +        if (resultCode == Activity.RESULT_OK) {
    +            when (requestCode) {
    +                QrCodeScannerActivity.QR_CODE_SCANNER_REQUEST_CODE -> {
    +                    val scannedQrCode = QrCodeScannerActivity.getResultText(data)
    +                    val wasQrCode = QrCodeScannerActivity.getResultIsQrCode(data)
    +
    +                    if (wasQrCode && !scannedQrCode.isNullOrBlank()) {
    +                        onRemoteQrCodeScanned(scannedQrCode)
    +                    } else {
    +                        Timber.w("It was not a QR code, or empty result")
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) {
    +        sharedViewModel.handle(VerificationAction.RemoteQrCodeScanned(
    +                it.otherUserMxItem?.id ?: "",
    +                it.pendingRequest?.transactionId ?: "",
    +                remoteQrCode
    +        ))
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    index a1d566d77b..87d237d9dc 100644
    --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt
    @@ -34,7 +34,9 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
     data class VerificationChooseMethodViewState(
             val otherUserId: String = "",
             val transactionId: String = "",
    -        val QRModeAvailable: Boolean = false,
    +        val otherCanShowQrCode: Boolean = false,
    +        val otherCanScanQrCode: Boolean = false,
    +        val QRtext: String? = null,
             val SASModeAvailable: Boolean = false
     ) : MvRxState
     
    @@ -49,13 +51,14 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
     
         override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
             val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
    -        val qrAvailable = pvr?.hasMethod(VerificationMethod.SCAN) ?: false
    -        val emojiAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
     
             setState {
                 copy(
    -                    QRModeAvailable = qrAvailable,
    -                    SASModeAvailable = emojiAvailable
    +                    otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
    +                    otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
    +                    // TODO
    +                    QRtext = "https://www.example.org",
    +                    SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
                 )
             }
         }
    @@ -84,13 +87,14 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
                 val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
                 val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
                 val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
    -            val qrAvailable = pvr?.hasMethod(VerificationMethod.SCAN) ?: false
    -            val emojiAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
     
                 return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
                         transactionId = args.verificationId ?: "",
    -                    QRModeAvailable = qrAvailable,
    -                    SASModeAvailable = emojiAvailable
    +                    otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
    +                    otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
    +                    // TODO
    +                    QRtext = "https://www.example.org",
    +                    SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
                 )
             }
         }
    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
    index 5163f5e8a8..4973f425cd 100644
    --- 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
    @@ -16,6 +16,7 @@
      */
     package im.vector.riotx.features.crypto.verification.epoxy
     
    +import android.graphics.Bitmap
     import android.widget.ImageView
     import androidx.core.view.ViewCompat
     import com.airbnb.epoxy.EpoxyAttribute
    @@ -33,11 +34,18 @@ abstract class BottomSheetVerificationBigImageItem : VectorEpoxyModel {
    -                    // These events are filtered from timeline in normal case
    -                    // Only visible in developer mode
    -                    noticeItemFactory.create(event, highlight, callback)
    +                    // TODO These are not filtered out by timeline when encrypted
    +                    // For now manually ignore
    +                    if (userPreferencesProvider.shouldShowHiddenEvents()) {
    +                        noticeItemFactory.create(event, highlight, callback)
    +                    } else null
                     }
                     EventType.KEY_VERIFICATION_CANCEL,
                     EventType.KEY_VERIFICATION_DONE         -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    index 5e24c69ad8..dc5bd740dd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
    @@ -108,7 +108,7 @@ class VerificationItemFactory @Inject constructor(
                                     .highlighted(highlight)
                                     .leftGuideline(avatarSizeProvider.leftGuideline)
                         }
    -                    else                     -> ignoredConclusion(event, highlight, callback)
    +                    else                     -> return ignoredConclusion(event, highlight, callback)
                     }
                 }
                 EventType.KEY_VERIFICATION_DONE   -> {
    diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
    index 6d92d36d38..a5e3f21f96 100644
    --- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
    @@ -120,6 +120,8 @@ object PopupAlertManager {
             }
             currentAlerter = next
             next?.let {
    +
    +            if (next.shouldBeDisplayedIn?.invoke(currentActivity) == false) return
                 val currentTime = System.currentTimeMillis()
                 if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) {
                     // skip
    diff --git a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    new file mode 100644
    index 0000000000..bb6f1be03b
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerActivity.kt
    @@ -0,0 +1,78 @@
    +/*
    + * Copyright 2020 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.riotx.features.qrcode
    +
    +import android.app.Activity
    +import android.content.Intent
    +import android.os.Bundle
    +import androidx.fragment.app.Fragment
    +import com.google.zxing.BarcodeFormat
    +import com.google.zxing.Result
    +import im.vector.riotx.R
    +import im.vector.riotx.core.di.ScreenComponent
    +import im.vector.riotx.core.extensions.replaceFragment
    +import im.vector.riotx.core.platform.VectorBaseActivity
    +
    +class QrCodeScannerActivity : VectorBaseActivity() {
    +
    +    override fun getLayoutRes() = R.layout.activity_simple
    +
    +    override fun injectWith(injector: ScreenComponent) {
    +        injector.inject(this)
    +    }
    +
    +    override fun onCreate(savedInstanceState: Bundle?) {
    +        super.onCreate(savedInstanceState)
    +        if (isFirstCreation()) {
    +            replaceFragment(R.id.simpleFragmentContainer, QrCodeScannerFragment::class.java)
    +        }
    +    }
    +
    +    fun setResultAndFinish(result: Result?) {
    +        result?.let {
    +            setResult(RESULT_OK, Intent().apply {
    +                putExtra(EXTRA_OUT_TEXT, it.text)
    +                putExtra(EXTRA_OUT_IS_QR_CODE, it.barcodeFormat == BarcodeFormat.QR_CODE)
    +            })
    +        }
    +        finish()
    +    }
    +
    +    companion object {
    +        private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
    +        private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE"
    +
    +        const val QR_CODE_SCANNER_REQUEST_CODE = 429
    +
    +        // For test only
    +        fun startForResult(activity: Activity, requestCode: Int = QR_CODE_SCANNER_REQUEST_CODE) {
    +            activity.startActivityForResult(Intent(activity, QrCodeScannerActivity::class.java), requestCode)
    +        }
    +
    +        fun startForResult(fragment: Fragment, requestCode: Int = QR_CODE_SCANNER_REQUEST_CODE) {
    +            fragment.startActivityForResult(Intent(fragment.requireActivity(), QrCodeScannerActivity::class.java), requestCode)
    +        }
    +
    +        fun getResultText(data: Intent?): String? {
    +            return data?.getStringExtra(EXTRA_OUT_TEXT)
    +        }
    +
    +        fun getResultIsQrCode(data: Intent?): Boolean {
    +            return data?.getBooleanExtra(EXTRA_OUT_IS_QR_CODE, false) == true
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt
    new file mode 100644
    index 0000000000..2c6e9ed3d5
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/features/qrcode/QrCodeScannerFragment.kt
    @@ -0,0 +1,51 @@
    +/*
    + * Copyright 2020 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.riotx.features.qrcode
    +
    +import com.google.zxing.Result
    +import im.vector.riotx.R
    +import im.vector.riotx.core.platform.VectorBaseFragment
    +import kotlinx.android.synthetic.main.fragment_qr_code_scanner.*
    +import me.dm7.barcodescanner.zxing.ZXingScannerView
    +import javax.inject.Inject
    +
    +class QrCodeScannerFragment @Inject constructor()
    +    : VectorBaseFragment(),
    +        ZXingScannerView.ResultHandler {
    +
    +    override fun getLayoutResId() = R.layout.fragment_qr_code_scanner
    +
    +    override fun onResume() {
    +        super.onResume()
    +        // Register ourselves as a handler for scan results.
    +        scannerView.setResultHandler(this)
    +        // Start camera on resume
    +        scannerView.startCamera()
    +    }
    +
    +    override fun onPause() {
    +        super.onPause()
    +        // Stop camera on pause
    +        scannerView.stopCamera()
    +    }
    +
    +    override fun handleResult(rawResult: Result?) {
    +        // Do something with the result here
    +        // This is not intended to be used outside of QrCodeScannerActivity for the moment
    +        (requireActivity() as? QrCodeScannerActivity)?.setResultAndFinish(rawResult)
    +    }
    +}
    diff --git a/vector/src/main/res/layout/fragment_qr_code_scanner.xml b/vector/src/main/res/layout/fragment_qr_code_scanner.xml
    new file mode 100644
    index 0000000000..589b7c73d4
    --- /dev/null
    +++ b/vector/src/main/res/layout/fragment_qr_code_scanner.xml
    @@ -0,0 +1,18 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    \ No newline at end of file
    diff --git a/vector/src/main/res/layout/item_verification_big_image.xml b/vector/src/main/res/layout/item_verification_big_image.xml
    index 9f33b6c03c..e4a0db917f 100644
    --- a/vector/src/main/res/layout/item_verification_big_image.xml
    +++ b/vector/src/main/res/layout/item_verification_big_image.xml
    @@ -1,7 +1,8 @@
     
     
    +    tools:src="@drawable/ic_shield_trusted" />
    diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
    index 0f7ad1a095..5faa105ec0 100644
    --- a/vector/src/main/res/values/strings_riotX.xml
    +++ b/vector/src/main/res/values/strings_riotX.xml
    @@ -43,7 +43,7 @@
         Can\'t scan
         If you\'re not in person, compare emoji instead
     
    -    Continue
    +    Verify by comparing emojis
     
         Verify by Emoji
         If you can’t scan the code above, verify by comparing a short, unique selection of emoji.