commit
e47791f290
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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", "/")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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) ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
holder.image.setImageResource(imageRes)
|
||||
imageBitmap?.let {
|
||||
holder.image.setImageBitmap(it)
|
||||
} ?: run {
|
||||
holder.image.setImageResource(imageRes)
|
||||
}
|
||||
|
||||
if (contentDescription == null) {
|
||||
ViewCompat.setImportantForAccessibility(holder.image, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotx.core.epoxy.EmptyItem_
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -29,7 +30,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory) {
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider ) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
|
@ -73,9 +75,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
// These events are filtered from timeline in normal case
|
||||
// Only visible in developer mode
|
||||
noticeItemFactory.create(event, highlight, callback)
|
||||
// TODO These are not filtered out by timeline when encrypted
|
||||
// For now manually ignore
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, highlight, callback)
|
||||
} else null
|
||||
}
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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 can’t scan the code above, verify by comparing a short, unique selection of emoji.</string>
|
||||
|
|
Loading…
Reference in New Issue