Merge pull request #888 from vector-im/qr_code

Qr code
This commit is contained in:
Valere 2020-01-23 16:41:23 +01:00 committed by GitHub
commit e47791f290
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 996 additions and 145 deletions

View file

@ -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)

View file

@ -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
}
}

View file

@ -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", "/")
}
}

View file

@ -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<CryptoDeviceInfo>

View file

@ -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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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 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)
}
/**

View file

@ -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

View file

@ -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,11 +82,12 @@ 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)
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
privateKeysInfo.master
?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading master key success")
} else {
@ -95,10 +95,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// TODO untrust
}
}
privateKeyinfo.user?.let { privateKey ->
val keySeed = Base64.decode(privateKey, Base64.NO_PADDING)
privateKeysInfo.user
?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading User Signing key success")
} else {
@ -106,10 +107,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// TODO untrust
}
}
privateKeyinfo.selfSigned?.let { privateKey ->
val keySeed = Base64.decode(privateKey, Base64.NO_PADDING)
privateKeysInfo.selfSigned
?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(keySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading Self Signing key success")
} else {
@ -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) ->

View file

@ -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)
}

View file

@ -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.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
)

View file

@ -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(

View file

@ -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")

View file

@ -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
@ -47,7 +48,8 @@ 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.QR_CODE_SHOW -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW)
VerificationMethod.QR_CODE_SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN)
}
}
}

View file

@ -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() {

View file

@ -37,8 +37,8 @@ internal interface SasTransport {
fun sendVerificationRequest(supportedMethods: List<String>,
localID: String,
otherUserId: String,
roomId: String, callback:
(String?, MessageVerificationRequestContent?) -> Unit)
roomId: String,
callback: (String?, MessageVerificationRequestContent?) -> Unit)
fun cancelTransaction(transactionId: String,
otherUserId: String,

View file

@ -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:
* <pre>
* https://matrix.to/#/<user-id>?
* request=<event-id>
* &action=verify
* &key_<keyid>=<key-in-base64>...
* &secret=<shared_secret>
* &other_user_key=<master-key-in-base64>
* </pre>
*/
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
)
}

View file

@ -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_<key_id>: 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<String, String>,
// 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"
}
}

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -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'

View file

@ -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<out String>, 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) ?: "")
}
}
}
}
}

View file

@ -68,6 +68,19 @@
android:layout_height="wrap_content"
android:text="Initialize XSigning" />
<com.google.android.material.button.MaterialButton
android:id="@+id/debug_scan_qr_code"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scan QR-code" />
<ImageView
android:id="@+id/debug_qr_code"
android:layout_width="200dp"
android:layout_height="200dp"
tools:src="@tools:sample/avatars" />
</LinearLayout>
</ScrollView>

View file

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".VectorApplication"
@ -123,13 +124,16 @@
</intent-filter>
</activity>
<activity android:name=".features.roommemberprofile.RoomMemberProfileActivity"
<activity
android:name=".features.roommemberprofile.RoomMemberProfileActivity"
android:parentActivityName=".features.home.HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".features.home.HomeActivity" />
</activity>
<activity android:name=".features.qrcode.QrCodeScannerActivity" />
<!-- Services -->
<service

View file

@ -364,6 +364,16 @@ SOFTWARE.
<br/>
Copyright 2017 Sergiy Kovalchuk
</li>
<li>
<b>ZXing</b>
<br/>
Copyright 2007 ZXing authors
</li>
<li>
<b>barcodescanner</b>
<br/>
Copyright (c) 2014 Dushyanth Maguluru
</li>
</ul>
<pre>
Apache License

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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"
}

View file

@ -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
)

View file

@ -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()
}

View file

@ -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,

View file

@ -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<VerificationBottomSheetViewState, VerificationAction>(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)

View file

@ -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,22 +46,28 @@ 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
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")
imageRes(R.drawable.riotx_logo)
imageBitmap(qrCodeBitmap)
}
dividerItem {
id("sep0")
}
}
if (state.otherCanShowQrCode) {
bottomSheetVerificationActionItem {
id("openCamera")
title(stringProvider.getString(R.string.verification_scan_their_code))
@ -71,6 +80,7 @@ class VerificationChooseMethodController @Inject constructor(
dividerItem {
id("sep1")
}
}
bottomSheetVerificationActionItem {
id("openEmoji")

View file

@ -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<out String>, 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
))
}
}

View file

@ -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
)
}
}

View file

@ -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<BottomShee
@EpoxyAttribute
var imageRes: Int = 0
@EpoxyAttribute
var imageBitmap: Bitmap? = null
@EpoxyAttribute
var contentDescription: String? = null
override fun bind(holder: Holder) {
imageBitmap?.let {
holder.image.setImageBitmap(it)
} ?: run {
holder.image.setImageResource(imageRes)
}
if (contentDescription == null) {
ViewCompat.setImportantForAccessibility(holder.image, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO)

View file

@ -1027,7 +1027,8 @@ class RoomDetailFragment @Inject constructor(
}
override fun onAvatarClicked(informationData: MessageInformationData) {
openRoomMemberProfile(informationData.senderId)
// DO NOT COMMIT openRoomMemberProfile(informationData.senderId)
roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
}
private fun openRoomMemberProfile(userId: String) {

View file

@ -823,6 +823,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
if (action.userId == session.myUserId) return
_requestLiveData.postValue(LiveEvent(Success(action)))
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.epoxy.EmptyItem_
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber
import javax.inject.Inject
@ -29,7 +30,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory) {
private val verificationConclusionItemFactory: VerificationItemFactory,
private val userPreferencesProvider: UserPreferencesProvider ) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
@ -73,9 +75,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case
// Only visible in developer mode
// 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 -> {

View file

@ -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 -> {

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/scannerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- TODO In the future we could add a toggle to switch the flash, and other possible settings -->
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemVerificationBigImage"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_margin="8dp"
android:src="@drawable/ic_shield_trusted" />
tools:src="@drawable/ic_shield_trusted" />

View file

@ -43,7 +43,7 @@
<string name="verification_scan_emoji_title">Can\'t scan</string>
<string name="verification_scan_emoji_subtitle">If you\'re not in person, compare emoji instead</string>
<string name="verification_no_scan_emoji_title">Continue</string>
<string name="verification_no_scan_emoji_title">Verify by comparing emojis</string>
<string name="verify_by_emoji_title">Verify by Emoji</string>
<string name="verify_by_emoji_description">If you cant scan the code above, verify by comparing a short, unique selection of emoji.</string>