Merge pull request #910 from vector-im/qr_code_step_2

Qr code step 2
This commit is contained in:
Valere 2020-01-28 18:11:09 +01:00 committed by GitHub
commit 9a79297e14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1314 additions and 480 deletions

View file

@ -51,8 +51,8 @@ class SASTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceSasMgr = aliceSession.getSasVerificationService()
val bobSasMgr = bobSession!!.getSasVerificationService()
val aliceVerificationService = aliceSession.getVerificationService()
val bobVerificationService = bobSession!!.getVerificationService()
val bobTxCreatedLatch = CountDownLatch(1)
val bobListener = object : VerificationService.VerificationListener {
@ -64,18 +64,18 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSasMgr.addListener(bobListener)
bobVerificationService.addListener(bobListener)
val txID = aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, bobSession.getMyDevice().deviceId)
val txID = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobSession.myUserId, bobSession.getMyDevice().deviceId)
assertNotNull("Alice should have a started transaction", txID)
val aliceKeyTx = aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID!!)
val aliceKeyTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID!!)
assertNotNull("Alice should have a started transaction", aliceKeyTx)
mTestHelper.await(bobTxCreatedLatch)
bobSasMgr.removeListener(bobListener)
bobVerificationService.removeListener(bobListener)
val bobKeyTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID)
val bobKeyTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID)
assertNotNull("Bob should have started verif transaction", bobKeyTx)
assertTrue(bobKeyTx is SASDefaultVerificationTransaction)
@ -105,7 +105,7 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSasMgr.addListener(bobListener2)
bobVerificationService.addListener(bobListener2)
aliceSasTx.cancel(CancelCode.User)
mTestHelper.await(cancelLatch)
@ -120,8 +120,8 @@ class SASTest : InstrumentedTest {
assertEquals("Should be User cancelled on bob side",
CancelCode.User, aliceSasTx.cancelledReason)
assertNull(bobSasMgr.getExistingTransaction(aliceSession.myUserId, txID))
assertNull(aliceSasMgr.getExistingTransaction(bobSession.myUserId, txID))
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID))
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID))
cryptoTestData.close()
}
@ -151,7 +151,7 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSession.getSasVerificationService().addListener(bobListener)
bobSession.getVerificationService().addListener(bobListener)
// TODO bobSession!!.dataHandler.addListener(object : MXEventListener() {
// TODO override fun onToDeviceEvent(event: Event?) {
@ -179,7 +179,7 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
aliceSession.getSasVerificationService().addListener(aliceListener)
aliceSession.getVerificationService().addListener(aliceListener)
fakeBobStart(bobSession, aliceUserID, aliceDevice, tid, protocols = protocols)
@ -303,7 +303,7 @@ class SASTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceSasMgr = aliceSession.getSasVerificationService()
val aliceVerificationService = aliceSession.getVerificationService()
val aliceCreatedLatch = CountDownLatch(2)
val aliceCancelledLatch = CountDownLatch(2)
@ -322,12 +322,12 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
aliceSasMgr.addListener(aliceListener)
aliceVerificationService.addListener(aliceListener)
val bobUserId = bobSession!!.myUserId
val bobDeviceId = bobSession.getMyDevice().deviceId
aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
mTestHelper.await(aliceCreatedLatch)
mTestHelper.await(aliceCancelledLatch)
@ -345,8 +345,8 @@ class SASTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceSasMgr = aliceSession.getSasVerificationService()
val bobSasMgr = bobSession!!.getSasVerificationService()
val aliceVerificationService = aliceSession.getVerificationService()
val bobVerificationService = bobSession!!.getVerificationService()
var accepted: KeyVerificationAccept? = null
var startReq: KeyVerificationStart? = null
@ -366,7 +366,7 @@ class SASTest : InstrumentedTest {
}
}
}
aliceSasMgr.addListener(aliceListener)
aliceVerificationService.addListener(aliceListener)
val bobListener = object : VerificationService.VerificationListener {
override fun transactionCreated(tx: VerificationTransaction) {}
@ -380,11 +380,11 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSasMgr.addListener(bobListener)
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.getMyDevice().deviceId
aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
mTestHelper.await(aliceAcceptedLatch)
assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
@ -409,17 +409,17 @@ class SASTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceSasMgr = aliceSession.getSasVerificationService()
val bobSasMgr = bobSession!!.getSasVerificationService()
val aliceVerificationService = aliceSession.getVerificationService()
val bobVerificationService = bobSession!!.getVerificationService()
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.VerificationListener {
override fun transactionCreated(tx: VerificationTransaction) {}
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationRequest).uxState
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
when (uxState) {
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
aliceSASLatch.countDown()
}
else -> Unit
@ -428,7 +428,7 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
aliceSasMgr.addListener(aliceListener)
aliceVerificationService.addListener(aliceListener)
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.VerificationListener {
@ -449,16 +449,16 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSasMgr.addListener(bobListener)
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.getMyDevice().deviceId
val verificationSAS = aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
val verificationSAS = aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
mTestHelper.await(aliceSASLatch)
mTestHelper.await(bobSASLatch)
val aliceTx = aliceSasMgr.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
val bobTx = bobSasMgr.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
val aliceTx = aliceVerificationService.getExistingTransaction(bobUserId, verificationSAS!!) as SASDefaultVerificationTransaction
val bobTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, verificationSAS) as SASDefaultVerificationTransaction
assertEquals("Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
bobTx.getShortCodeRepresentation(SasMode.DECIMAL))
@ -473,20 +473,20 @@ class SASTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession
val aliceSasMgr = aliceSession.getSasVerificationService()
val bobSasMgr = bobSession!!.getSasVerificationService()
val aliceVerificationService = aliceSession.getVerificationService()
val bobVerificationService = bobSession!!.getVerificationService()
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.VerificationListener {
override fun transactionCreated(tx: VerificationTransaction) {}
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationRequest).uxState
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
when (uxState) {
OutgoingSasVerificationRequest.UxState.SHOW_SAS -> {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode()
}
OutgoingSasVerificationRequest.UxState.VERIFIED -> {
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
aliceSASLatch.countDown()
}
else -> Unit
@ -495,7 +495,7 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
aliceSasMgr.addListener(aliceListener)
aliceVerificationService.addListener(aliceListener)
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.VerificationListener {
@ -519,11 +519,11 @@ class SASTest : InstrumentedTest {
override fun markedAsManuallyVerified(userId: String, deviceId: String) {}
}
bobSasMgr.addListener(bobListener)
bobVerificationService.addListener(bobListener)
val bobUserId = bobSession.myUserId
val bobDeviceId = bobSession.getMyDevice().deviceId
aliceSasMgr.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
aliceVerificationService.beginKeyVerification(VerificationMethod.SAS, bobUserId, bobDeviceId)
mTestHelper.await(aliceSASLatch)
mTestHelper.await(bobSASLatch)

View file

@ -50,7 +50,7 @@ interface CryptoService {
fun isCryptoEnabled(): Boolean
fun getSasVerificationService(): VerificationService
fun getVerificationService(): VerificationService
fun getCrossSigningService(): CrossSigningService
@ -119,6 +119,7 @@ interface CryptoService {
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>)
fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>>
fun addNewSessionListener(newSessionListener: NewSessionListener)

View file

@ -26,33 +26,41 @@ import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
interface CrossSigningService {
fun isUserTrusted(userId: String) : Boolean
fun isCrossSigningEnabled(): Boolean
fun isUserTrusted(otherUserId: String): Boolean
/**
* Will not force a download of the key, but will verify signatures trust chain.
* Checks that my trusted user key has signed the other user UserKey
*/
fun checkUserTrust(userId: String) : UserTrustResult
fun checkUserTrust(otherUserId: String): UserTrustResult
/**
* Initialize cross signing for this user.
* Users needs to enter credentials
*/
fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>? = null)
fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null)
fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo?
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>>
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
fun canCrossSign(): Boolean
fun trustUser(userId: String, callback: MatrixCallback<Unit>)
fun trustUser(otherUserId: String,
callback: MatrixCallback<Unit>)
/**
* Sign one of your devices and upload the signature
*/
fun signDevice(deviceId: String, callback: MatrixCallback<Unit>)
fun signDevice(deviceId: String,
callback: MatrixCallback<Unit>)
fun checkDeviceTrust(userId: String, deviceId: String, locallyTrusted: Boolean?) : DeviceTrustResult
fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
}

View file

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO Rename package
package im.vector.matrix.android.api.session.crypto.sas
enum class CancelCode(val value: String, val humanReadable: String) {
@ -25,7 +27,9 @@ enum class CancelCode(val value: String, val humanReadable: String) {
UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"),
InvalidMessage("m.invalid_message", "an invalid message was received"),
MismatchedKeys("m.key_mismatch", "Key mismatch"),
UserMismatchError("m.user_error", "User mismatch")
UserError("m.user_error", "User error"),
MismatchedUser("m.user_mismatch", "User mismatch"),
QrCodeInvalid("m.qr_code.invalid", "Invalid QR code")
}
fun safeValueOf(code: String?): CancelCode {

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.api.session.crypto.sas
interface OutgoingSasVerificationRequest : SasVerificationTransaction {
interface OutgoingSasVerificationTransaction : SasVerificationTransaction {
val uxState: UxState
enum class UxState {

View file

@ -1,7 +0,0 @@
package im.vector.matrix.android.api.session.crypto.sas
interface QRVerificationTransaction : VerificationTransaction {
fun userHasScannedRemoteQrCode(scannedData: String)
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.crypto.sas
interface QrCodeVerificationTransaction : VerificationTransaction {
/**
* To use to display a qr code, for the other user to scan it
*/
val qrCodeText: String?
/**
* Call when you have scan the other user QR code
*/
fun userHasScannedOtherQrCode(otherQrCodeText: String)
}

View file

@ -24,8 +24,8 @@ import im.vector.matrix.android.internal.crypto.verification.PendingVerification
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
*
* Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors.
* SAS verification is a user-friendly key verification process.
* SAS verification is intended to be a highly interactive process for users,
* Verification is a user-friendly key verification process.
* Verification is intended to be a highly interactive process for users,
* and as such exposes verification methods which are easier for users to use.
*/
interface VerificationService {
@ -39,23 +39,24 @@ interface VerificationService {
*/
fun markedLocallyAsManuallyVerified(userId: String, deviceID: String)
fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction?
fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction?
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>?
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest?
fun getExistingVerificationRequestInRoom(roomId: String, tid: String?): PendingVerificationRequest?
fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String?
fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String?
/**
* Request a key verification from another user using toDevice events.
*/
fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, userId: String, roomId: String, localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, otherUserId: String, roomId: String, localId: String? = LocalEcho.createLocalEchoId()): PendingVerificationRequest
fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String)
// Only SAS method is supported for the moment
fun beginKeyVerificationInDMs(method: VerificationMethod,
transactionId: String,
roomId: String,
@ -66,7 +67,10 @@ interface VerificationService {
/**
* Returns false if the request is unknown
*/
fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean
fun readyPendingVerificationInDMs(methods: List<VerificationMethod>,
otherUserId: String,
roomId: String,
transactionId: String): Boolean
// fun transactionUpdated(tx: SasVerificationTransaction)

View file

@ -1,19 +1,37 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.crypto.sas
interface VerificationTransaction {
var state: VerificationTxState
val cancelledReason: CancelCode?
val transactionId: String
val otherUserId: String
var otherDeviceId: String?
// TODO Not used. Remove?
val isIncoming: Boolean
/**
* User wants to cancel the transaction
*/
fun cancel()
fun cancel(code: CancelCode)
fun isToDeviceTransport(): Boolean

View file

@ -44,6 +44,8 @@ enum class VerificationTxState {
Verified,
// Global: The verification has been cancelled (by me or other), see cancelReason for details
// When I do the cancel
Cancelled,
// When the other user do a cancel
OnCancelled
}

View file

@ -20,7 +20,8 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.sas.SasMode
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.supportedVerificationMethods
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer
@ -34,7 +35,8 @@ internal data class MessageVerificationStartContent(
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>?,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>?,
@Json(name = "method") override val method: String?,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?,
@Json(name = "secret") override val sharedSecret: String?
) : VerificationInfoStart {
override fun toCanonicalJson(): String? {
@ -44,22 +46,39 @@ internal data class MessageVerificationStartContent(
override val transactionID: String?
get() = relatesTo?.eventId
// TODO Move those method to the interface?
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank()
|| method !in supportedVerificationMethods
|| keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
Timber.e("## received invalid verification request")
return false
}
return true
}
private fun isValidSas(): Boolean {
if (keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
return false
}
return true
}
private fun isValidReciprocate(): Boolean {
if (sharedSecret.isNullOrBlank()) {
return false
}
return true
}
override fun toEventContent() = toContent()
}

View file

@ -30,3 +30,7 @@ const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
* Matrix algorithm value for megolm keys backup.
*/
const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
// TODO Refacto: use this constants everywhere
const val ed25519 = "ed25519"
const val curve25519 = "curve25519"

View file

@ -126,8 +126,8 @@ internal class DefaultCryptoService @Inject constructor(
private val oneTimeKeysUploader: OneTimeKeysUploader,
//
private val roomDecryptorProvider: RoomDecryptorProvider,
// The SAS verification service.
private val sasVerificationService: DefaultVerificationService,
// The verification service.
private val verificationService: DefaultVerificationService,
private val crossSigningService: DefaultCrossSigningService,
//
@ -157,7 +157,7 @@ internal class DefaultCryptoService @Inject constructor(
init {
sasVerificationService.cryptoService = this
verificationService.cryptoService = this
}
private val uiHandler = Handler(Looper.getMainLooper())
@ -343,7 +343,7 @@ internal class DefaultCryptoService @Inject constructor(
/**
* @return the VerificationService
*/
override fun getSasVerificationService() = sasVerificationService
override fun getVerificationService() = verificationService
override fun getCrossSigningService() = crossSigningService

View file

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
@ -41,6 +40,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
@ -50,7 +50,6 @@ import javax.inject.Inject
@SessionScope
internal class DefaultCrossSigningService @Inject constructor(
@UserId private val userId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val olmDevice: MXOlmDevice,
@ -145,8 +144,6 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
Timber.d("## CrossSigning initializeCrossSigning")
val myUserID = credentials.userId
// =================
// MASTER KEY
// =================
@ -166,7 +163,7 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
// Sign userSigningKey with master
val signedUSK = CryptoCrossSigningKey.Builder(myUserID, KeyUsage.USER_SIGNING)
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.build()
.canonicalSignable()
@ -182,23 +179,23 @@ internal class DefaultCrossSigningService @Inject constructor(
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
// Sign userSigningKey with master
val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CryptoCrossSigningKey.Builder(myUserID, KeyUsage.SELF_SIGNING)
val signedSSK = JsonCanonicalizer.getCanonicalJson(Map::class.java, CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.build().signalableJSONDictionary()).let { masterPkOlm.sign(it) }
// I need to upload the keys
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(myUserID, KeyUsage.MASTER)
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
.key(masterPublicKey)
.build()
val params = UploadSigningKeysTask.Params(
masterKey = mskCrossSigningKeyInfo,
userKey = CryptoCrossSigningKey.Builder(myUserID, KeyUsage.USER_SIGNING)
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
.key(uskPublicKey)
.signature(myUserID, masterPublicKey, signedUSK)
.signature(userId, masterPublicKey, signedUSK)
.build(),
selfSignedKey = CryptoCrossSigningKey.Builder(myUserID, KeyUsage.SELF_SIGNING)
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
.key(sskPublicKey)
.signature(myUserID, masterPublicKey, signedSSK)
.signature(userId, masterPublicKey, signedSSK)
.build(),
userPasswordAuth = authParams
)
@ -207,16 +204,16 @@ internal class DefaultCrossSigningService @Inject constructor(
this.userPkSigning = userSigningPkOlm
this.selfSigningPkSigning = selfSigningPkOlm
val crossSigningInfo = MXCrossSigningInfo(myUserID, listOf(params.masterKey, params.userKey, params.selfSignedKey))
val crossSigningInfo = MXCrossSigningInfo(userId, listOf(params.masterKey, params.userKey, params.selfSignedKey))
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
cryptoStore.setUserKeysAsTrusted(myUserID)
cryptoStore.setUserKeysAsTrusted(userId)
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
uploadSigningKeysTask.configureWith(params) {
this.constraints = TaskConstraints(true)
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - Keys succesfully uploaded")
Timber.i("## CrossSigning - Keys successfully uploaded")
// Sign the current device with SSK
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
@ -225,7 +222,7 @@ internal class DefaultCrossSigningService @Inject constructor(
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap()).also {
it[myUserID] = (it[myUserID]
it[userId] = (it[userId]
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
}
myDevice.copy(signatures = updateSignatures).let {
@ -236,7 +233,7 @@ internal class DefaultCrossSigningService @Inject constructor(
olmDevice.signMessage(JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary()))?.let { sign ->
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
?: HashMap()).also {
it[myUserID] = (it[myUserID]
it[userId] = (it[userId]
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
}
mskCrossSigningKeyInfo.copy(
@ -252,7 +249,7 @@ internal class DefaultCrossSigningService @Inject constructor(
this.constraints = TaskConstraints(true)
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - signatures succesfuly uploaded")
Timber.i("## CrossSigning - signatures successfully uploaded")
callback?.onSuccess(Unit)
}
@ -299,27 +296,26 @@ internal class DefaultCrossSigningService @Inject constructor(
*
* MSK MSK
*
*
* SSK SSK
*
* USK
* USK (not visible by
* Alice)
*
*
* BOB's Device
*
*
* SSK
*
*
* USK
*/
override fun isUserTrusted(userId: String): Boolean {
override fun isUserTrusted(otherUserId: String): Boolean {
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
}
override fun isCrossSigningEnabled(): Boolean {
return checkSelfTrust().isVerified()
}
/**
* Will not force a download of the key, but will verify signatures trust chain
*/
override fun checkUserTrust(userId: String): UserTrustResult {
Timber.d("## CrossSigning checkUserTrust for $userId")
if (userId == credentials.userId) {
override fun checkUserTrust(otherUserId: String): UserTrustResult {
Timber.d("## CrossSigning checkUserTrust for $otherUserId")
if (otherUserId == userId) {
return checkSelfTrust()
}
// I trust a user if I trust his master key
@ -327,25 +323,25 @@ internal class DefaultCrossSigningService @Inject constructor(
// TODO what if the master key is signed by a device key that i have verified
// First let's get my user key
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(credentials.userId)
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(credentials.userId)
?: return UserTrustResult.CrossSigningNotConfigured(userId)
if (!myCrossSigningInfo.isTrusted()) {
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
}
// Let's get the other user master key
val otherMasterKey = cryptoStore.getCrossSigningInfo(userId)?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(userId)
val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(credentials.userId) // Signatures made by me
?.get(userId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $userId, not signed by my UserSigningKey")
Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
return UserTrustResult.KeyNotSigned(otherMasterKey)
}
@ -363,12 +359,10 @@ internal class DefaultCrossSigningService @Inject constructor(
// Special case when it's me,
// I have to check that MSK -> USK -> SSK
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
val myUserId = credentials.userId
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
val myMasterKey = myCrossSigningInfo?.masterKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
?: return UserTrustResult.CrossSigningNotConfigured(userId)
// Is the master key trusted
// 1) check if I know the private key
@ -390,9 +384,9 @@ internal class DefaultCrossSigningService @Inject constructor(
olmPkSigning?.releaseSigning()
} else {
// Maybe it's signed by a locally trusted device?
myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) ->
val potentialDeviceId = if (key.startsWith("ed25519:")) key.substring("ed25519:".length) else key
val potentialDevice = cryptoStore.getUserDevice(myUserId, potentialDeviceId)
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
val potentialDeviceId = key.withoutPrefix("ed25519:")
val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity?
try {
@ -412,10 +406,10 @@ internal class DefaultCrossSigningService @Inject constructor(
}
val myUserKey = myCrossSigningInfo.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
?: return UserTrustResult.CrossSigningNotConfigured(userId)
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
?.get(myUserId) // Signatures made by me
?.get(userId) // Signatures made by me
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
@ -431,29 +425,29 @@ internal class DefaultCrossSigningService @Inject constructor(
}
val mySSKey = myCrossSigningInfo.selfSigningKey()
?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
?: return UserTrustResult.CrossSigningNotConfigured(userId)
val SSKeySignaturesMadeByMyMasterKey = mySSKey.signatures
?.get(myUserId) // Signatures made by me
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
?.get(userId) // Signatures made by me
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
if (SSKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK")
return UserTrustResult.KeyNotSigned(mySSKey)
}
// Check that Alice USK signature of Alice MSK is valid
try {
olmUtility!!.verifyEd25519Signature(SSKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
} catch (failure: Throwable) {
return UserTrustResult.InvalidSignature(mySSKey, SSKeySignaturesMadeByMyMasterKey)
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
}
return UserTrustResult.Success
}
override fun getUserCrossSigningKeys(userId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(userId)
override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(otherUserId)
}
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
@ -468,15 +462,15 @@ internal class DefaultCrossSigningService @Inject constructor(
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
}
override fun trustUser(userId: String, callback: MatrixCallback<Unit>) {
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
Timber.d("## CrossSigning - Mark user $userId as trusted ")
// We should have this user keys
val otherMasterKeys = getUserCrossSigningKeys(userId)?.masterKey()
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
if (otherMasterKeys == null) {
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.userId)
val myKeys = getUserCrossSigningKeys(userId)
if (myKeys == null) {
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
return
@ -487,7 +481,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return
}
// Sign the other MasterKey with our UserSiging key
// Sign the other MasterKey with our UserSigning key
val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java,
otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) }
@ -497,12 +491,12 @@ internal class DefaultCrossSigningService @Inject constructor(
return
}
cryptoStore.setUserKeysAsTrusted(userId, true)
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
// TODO update local copy with new signature directly here? kind of local echo of trust?
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
val uploadQuery = UploadSignatureQueryBuilder()
.withSigningKeyInfo(otherMasterKeys.copyForSignature(credentials.userId, userPubKey, newSignature))
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.callback = callback
@ -511,13 +505,13 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun signDevice(deviceId: String, callback: MatrixCallback<Unit>) {
// This device should be yours
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
val device = cryptoStore.getUserDevice(userId, deviceId)
if (device == null) {
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
return
}
val myKeys = getUserCrossSigningKeys(credentials.userId)
val myKeys = getUserCrossSigningKeys(userId)
if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
return
@ -539,7 +533,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
val toUpload = device.copy(
signatures = mapOf(
credentials.userId
userId
to
mapOf(
"ed25519:$ssPubKey" to newSignature
@ -555,17 +549,17 @@ internal class DefaultCrossSigningService @Inject constructor(
}.executeBy(taskExecutor)
}
override fun checkDeviceTrust(userId: String, deviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
val otherDevice = cryptoStore.getUserDevice(userId, deviceId)
?: return DeviceTrustResult.UnknownDevice(deviceId)
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
val myKeys = getUserCrossSigningKeys(credentials.userId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(credentials.userId))
val myKeys = getUserCrossSigningKeys(userId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
val otherKeys = getUserCrossSigningKeys(userId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
val otherKeys = getUserCrossSigningKeys(otherUserId)
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId))
// TODO should we force verification ?
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
@ -589,15 +583,15 @@ internal class DefaultCrossSigningService @Inject constructor(
*
*/
val otherSSKSignature = otherDevice.signatures?.get(userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.MissingDeviceSignature(deviceId, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey
val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.MissingDeviceSignature(otherDeviceId, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey
?: ""))
// Check bob's device is signed by bob's SSK
try {
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
} catch (e: Throwable) {
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(deviceId, otherSSKSignature, e))
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
}
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
@ -614,19 +608,19 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun onUsersDeviceUpdate(users: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users")
users.forEach { userId ->
users.forEach { otherUserId ->
checkUserTrust(userId).let {
Timber.d("## CrossSigning - update trust for ${userId} , verified=${it.isVerified()}")
cryptoStore.setUserKeysAsTrusted(userId, it.isVerified())
checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
cryptoStore.setUserKeysAsTrusted(otherUserId, it.isVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(userId)
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user ${userId} , verified=$updatedTrust")
cryptoStore.setDeviceTrust(userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}

View file

@ -34,30 +34,48 @@ internal data class KeyVerificationStart(
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
@Json(name = "hashes") override val hashes: List<String>? = null,
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null,
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null
@Json(name = "short_authentication_string") override val shortAuthenticationStrings: List<String>? = null,
// For QR code verification
@Json(name = "secret") override val sharedSecret: String? = null
) : SendToDeviceObject, VerificationInfoStart {
override fun toCanonicalJson(): String? {
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
}
// TODO Move those method to the interface?
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank()
|| method !in supportedVerificationMethods
|| keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256")
|| messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
Timber.e("## received invalid verification request")
return false
}
return true
}
private fun isValidSas(): Boolean {
if (keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
return false
}
return true
}
private fun isValidReciprocate(): Boolean {
if (sharedSecret.isNullOrBlank()) {
return false
}
return true
}
override fun toSendToDeviceObject() = this
}

View file

@ -33,11 +33,3 @@ internal fun VerificationMethod.toValue(): String {
VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW
}
}
internal val supportedVerificationMethods =
listOf(
VERIFICATION_METHOD_SAS,
VERIFICATION_METHOD_QR_CODE_SHOW,
VERIFICATION_METHOD_QR_CODE_SCAN,
VERIFICATION_METHOD_RECIPROCATE
)

View file

@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.verification
import android.util.Base64
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
@ -30,23 +29,25 @@ import timber.log.Timber
internal class DefaultIncomingSASDefaultVerificationTransaction(
setDeviceVerificationAction: SetDeviceVerificationAction,
override val credentials: Credentials,
override val userId: String,
override val deviceId: String?,
private val cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService,
deviceFingerprint: String,
transactionId: String,
otherUserID: String,
val autoAccept: Boolean = false
private val autoAccept: Boolean = false
) : SASDefaultVerificationTransaction(
setDeviceVerificationAction,
credentials,
userId,
deviceId,
cryptoStore,
crossSigningService,
deviceFingerprint,
transactionId,
otherUserID,
null,
true),
isIncoming = true),
IncomingSasVerificationTransaction {
override val uxState: IncomingSasVerificationTransaction.UxState
@ -157,7 +158,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
cancel(CancelCode.UnexpectedMessage)
}
override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) {
override fun onKeyVerificationKey(vKey: VerificationInfoKey) {
Timber.v("## SAS received key for request id:$transactionId")
if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) {
Timber.e("## SAS received key from invalid state $state")
@ -194,10 +195,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" +
"$otherUserId$otherDeviceId" +
"${credentials.userId}${credentials.deviceId}" +
transactionId
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)

View file

@ -15,21 +15,19 @@
*/
package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest
import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.rest.toValue
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import timber.log.Timber
internal class DefaultOutgoingSASDefaultVerificationRequest(
internal class DefaultOutgoingSASDefaultVerificationTransaction(
setDeviceVerificationAction: SetDeviceVerificationAction,
credentials: Credentials,
userId: String,
deviceId: String?,
cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService,
deviceFingerprint: String,
@ -38,7 +36,8 @@ internal class DefaultOutgoingSASDefaultVerificationRequest(
otherDeviceId: String
) : SASDefaultVerificationTransaction(
setDeviceVerificationAction,
credentials,
userId,
deviceId,
cryptoStore,
crossSigningService,
deviceFingerprint,
@ -46,27 +45,27 @@ internal class DefaultOutgoingSASDefaultVerificationRequest(
otherUserId,
otherDeviceId,
isIncoming = false),
OutgoingSasVerificationRequest {
OutgoingSasVerificationTransaction {
override val uxState: OutgoingSasVerificationRequest.UxState
override val uxState: OutgoingSasVerificationTransaction.UxState
get() {
return when (state) {
VerificationTxState.None -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_START
VerificationTxState.None -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_START
VerificationTxState.SendingStart,
VerificationTxState.Started,
VerificationTxState.OnAccepted,
VerificationTxState.SendingKey,
VerificationTxState.KeySent,
VerificationTxState.OnKeyReceived -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT
VerificationTxState.ShortCodeReady -> OutgoingSasVerificationRequest.UxState.SHOW_SAS
VerificationTxState.OnKeyReceived -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
VerificationTxState.ShortCodeReady -> OutgoingSasVerificationTransaction.UxState.SHOW_SAS
VerificationTxState.ShortCodeAccepted,
VerificationTxState.SendingMac,
VerificationTxState.MacSent,
VerificationTxState.Verifying -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION
VerificationTxState.Verified -> OutgoingSasVerificationRequest.UxState.VERIFIED
VerificationTxState.OnCancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME
VerificationTxState.Cancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER
else -> OutgoingSasVerificationRequest.UxState.UNKNOWN
VerificationTxState.Verifying -> OutgoingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
VerificationTxState.Verified -> OutgoingSasVerificationTransaction.UxState.VERIFIED
VerificationTxState.OnCancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_ME
VerificationTxState.Cancelled -> OutgoingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
else -> OutgoingSasVerificationTransaction.UxState.UNKNOWN
}
}
@ -75,16 +74,15 @@ internal class DefaultOutgoingSASDefaultVerificationRequest(
cancel(CancelCode.UnexpectedMessage)
}
fun start(method: VerificationMethod) {
fun start() {
if (state != VerificationTxState.None) {
Timber.e("## SAS O: start verification from invalid state")
// should I cancel??
throw IllegalStateException("Interactive Key verification already started")
}
val startMessage = transport.createStart(
credentials.deviceId ?: "",
method.toValue(),
val startMessage = transport.createStartForSas(
deviceId ?: "",
transactionId,
KNOWN_AGREEMENT_PROTOCOLS,
KNOWN_HASHES,
@ -164,7 +162,7 @@ internal class DefaultOutgoingSASDefaultVerificationRequest(
}
}
override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) {
override fun onKeyVerificationKey(vKey: VerificationInfoKey) {
Timber.v("## SAS O: onKeyVerificationKey id:$transactionId")
if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) {
Timber.e("## received key from invalid state $state")
@ -192,16 +190,13 @@ internal class DefaultOutgoingSASDefaultVerificationRequest(
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" +
"${credentials.userId}${credentials.deviceId}" +
"$otherUserId$otherDeviceId" +
transactionId
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
state = VerificationTxState.ShortCodeReady
} else {
// bad commitement
// bad commitment
cancel(CancelCode.MismatchedCommitment)
}
}

View file

@ -20,45 +20,74 @@ import android.os.Handler
import android.os.Looper
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.sas.*
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import im.vector.matrix.android.internal.crypto.model.rest.toValue
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.set
@SessionScope
internal class DefaultVerificationService @Inject constructor(
private val credentials: Credentials,
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val cryptoStore: IMXCryptoStore,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val deviceListManager: DeviceListManager,
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory,
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
private val crossSigningService: CrossSigningService
) : DefaultVerificationTransaction.Listener, VerificationService {
@ -236,7 +265,7 @@ internal class DefaultVerificationService @Inject constructor(
?: return
val senderId = event.senderId ?: return
if (requestInfo.toUserId != credentials.userId) {
if (requestInfo.toUserId != userId) {
// I should ignore this, it's not for me
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
return
@ -289,7 +318,7 @@ internal class DefaultVerificationService @Inject constructor(
if (startReq?.isValid()?.not() == true) {
Timber.e("## received invalid verification request")
if (startReq.transactionID != null) {
sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
@ -301,9 +330,9 @@ internal class DefaultVerificationService @Inject constructor(
}
handleStart(otherUserId, startReq as VerificationInfoStart) {
it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
}?.let {
sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
@ -322,7 +351,7 @@ internal class DefaultVerificationService @Inject constructor(
if (!startReq.isValid()) {
Timber.e("## SAS received invalid verification request")
if (startReq.transactionID != null) {
sasTransportToDeviceFactory.createTransport(null).cancelTransaction(
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID,
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
@ -333,9 +362,9 @@ internal class DefaultVerificationService @Inject constructor(
}
// Download device keys prior to everything
handleStart(otherUserId, startReq) {
it.transport = sasTransportToDeviceFactory.createTransport(it)
it.transport = verificationTransportToDeviceFactory.createTransport(it)
}?.let {
sasTransportToDeviceFactory.createTransport(null).cancelTransaction(
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
@ -344,30 +373,46 @@ internal class DefaultVerificationService @Inject constructor(
}
}
/**
* Return a CancelCode to make the caller cancel the verification. Else return null
*/
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID}")
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
Timber.v("## SAS onStartRequestReceived $startReq")
val tid = startReq.transactionID!!
val existing = getExistingTransaction(otherUserId, tid)
val existingTxs = getExistingTransactionsForUser(otherUserId)
if (existing != null) {
when (startReq.method) {
VERIFICATION_METHOD_SAS -> {
when (existing) {
is SasVerificationTransaction -> {
// should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}")
Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionID}")
existing.cancel(CancelCode.UnexpectedMessage)
return CancelCode.UnexpectedMessage
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else if (existingTxs?.isEmpty() == false) {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}")
// Already cancelled, so return null
return null
}
is QrCodeVerificationTransaction -> {
// Nothing to do?
}
null -> {
getExistingTransactionsForUser(otherUserId)
?.filterIsInstance(SasVerificationTransaction::class.java)
?.takeIf { it.isNotEmpty() }
?.also {
// Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time.
existingTxs.forEach {
Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID}")
}
?.forEach {
it.cancel(CancelCode.UnexpectedMessage)
}
?.also {
return CancelCode.UnexpectedMessage
// cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
} else {
// Ok we can create
if (startReq.method == VERIFICATION_METHOD_SAS) {
}
}
}
// Ok we can create a SAS transaction
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
// If there is a corresponding request, we can auto accept
// as we are the one requesting in first place (or we accepted the request)
@ -376,7 +421,8 @@ internal class DefaultVerificationService @Inject constructor(
val tx = DefaultIncomingSASDefaultVerificationTransaction(
// this,
setDeviceVerificationAction,
credentials,
userId,
deviceId,
cryptoStore,
crossSigningService,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
@ -385,30 +431,39 @@ internal class DefaultVerificationService @Inject constructor(
autoAccept).also { txConfigure(it) }
addTransaction(tx)
tx.acceptVerificationEvent(otherUserId, startReq)
return null
}
VERIFICATION_METHOD_RECIPROCATE -> {
// Other user has scanned my QR code
if (existing is DefaultQrCodeVerificationTransaction) {
existing.onStartReceived(startReq)
return null
} else {
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionID}")
return CancelCode.UnexpectedMessage
}
}
else ->{
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
return CancelCode.UnknownMethod
// cancelTransaction(tid, otherUserId, startReq.fromDevice
// ?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
} else {
return CancelCode.UnexpectedMessage
// cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage)
}
return null
}
// TODO Refacto: It could just return a boolean
private suspend fun checkKeysAreDownloaded(otherUserId: String,
fromDevice: String): MXUsersDevicesMap<CryptoDeviceInfo>? {
otherDeviceId: String): MXUsersDevicesMap<CryptoDeviceInfo>? {
return try {
var keys = deviceListManager.downloadKeys(listOf(otherUserId), false)
if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) {
if (keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true) {
return keys
} else {
// force download
keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true }
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(otherDeviceId) == true }
}
} catch (e: Exception) {
null
@ -573,12 +628,12 @@ internal class DefaultVerificationService @Inject constructor(
return
}
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known")
// TODO cancel?
return
}
handleReadyReceived(event.senderId, readyReq)
handleReadyReceived(event.senderId, event.roomId!!, readyReq)
}
private fun onRoomDoneReceived(event: Event) {
@ -623,13 +678,87 @@ internal class DefaultVerificationService @Inject constructor(
}
}
private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) {
private fun handleReadyReceived(senderId: String, roomId: String, readyReq: VerificationInfoReady) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
if (existingRequest == null) {
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}")
return
}
updatePendingRequest(existingRequest.copy(readyInfo = readyReq))
val qrCodeData = readyReq.methods
// Check if other user is able to scan QR code
?.takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) }
?.let {
createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId)
}
if (readyReq.methods?.orEmpty().orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE)) {
// Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
readyReq.transactionID!!,
senderId,
readyReq.fromDevice,
crossSigningService,
cryptoStore,
qrCodeData,
userId,
deviceId ?: "",
false)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
addTransaction(tx)
}
updatePendingRequest(existingRequest.copy(
readyInfo = readyReq
))
}
private fun createQrCodeData(transactionId: String?, otherUserId: String): QrCodeData? {
// Build the QR code URL
val requestEventId = transactionId ?: run {
Timber.w("## Unknown requestEventId")
return null
}
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey()
?.unpaddedBase64PublicKey
?: run {
Timber.w("## Unable to get my master key")
return null
}
val otherUserMasterKey = crossSigningService.getUserCrossSigningKeys(otherUserId)
?.masterKey()
?.unpaddedBase64PublicKey
?: run {
Timber.w("## Unable to get other user master key")
return null
}
val myDeviceId = deviceId
?: run {
Timber.w("## Unable to get my deviceId")
return null
}
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()!!
val generatedSharedSecret = generateSharedSecret()
return QrCodeData(
userId = userId,
requestEventId = requestEventId,
action = QrCodeData.ACTION_VERIFY,
keys = hashMapOf(
myMasterKey to myMasterKey,
myDeviceId to myDeviceKey
),
sharedSecret = generatedSharedSecret,
otherUserKey = otherUserMasterKey
)
}
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) {
@ -641,21 +770,22 @@ internal class DefaultVerificationService @Inject constructor(
updatePendingRequest(existingRequest.copy(isSuccessful = true))
}
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
// TODO All this methods should be delegated to a TransactionStore
override fun getExistingTransaction(otherUserId: String, tid: String): VerificationTransaction? {
synchronized(lock = txMap) {
return txMap[otherUser]?.get(tid)
return txMap[otherUserId]?.get(tid)
}
}
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
override fun getExistingVerificationRequest(otherUserId: String): List<PendingVerificationRequest>? {
synchronized(lock = pendingRequests) {
return pendingRequests[otherUser]
return pendingRequests[otherUserId]
}
}
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
override fun getExistingVerificationRequest(otherUserId: String, tid: String?): PendingVerificationRequest? {
synchronized(lock = pendingRequests) {
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
return tid?.let { tid -> pendingRequests[otherUserId]?.firstOrNull { it.transactionId == tid } }
}
}
@ -692,39 +822,40 @@ internal class DefaultVerificationService @Inject constructor(
}
}
override fun beginKeyVerification(method: VerificationMethod, userId: String, deviceID: String): String? {
val txID = createUniqueIDForTransaction(userId, deviceID)
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceID: String): String? {
val txID = createUniqueIDForTransaction(otherUserId, otherDeviceID)
// should check if already one (and cancel it)
if (method == VerificationMethod.SAS) {
val tx = DefaultOutgoingSASDefaultVerificationRequest(
val tx = DefaultOutgoingSASDefaultVerificationTransaction(
setDeviceVerificationAction,
credentials,
userId,
deviceId,
cryptoStore,
crossSigningService,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID,
userId,
deviceID)
tx.transport = sasTransportToDeviceFactory.createTransport(tx)
otherUserId,
otherDeviceID)
tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
addTransaction(tx)
tx.start(method)
tx.start()
return txID
} else {
throw IllegalArgumentException("Unknown verification method")
}
}
override fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, userId: String, roomId: String, localId: String?)
override fun requestKeyVerificationInDMs(methods: List<VerificationMethod>, otherUserId: String, roomId: String, localId: String?)
: PendingVerificationRequest {
Timber.i("## SAS Requesting verification to user: $userId in room $roomId")
Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId")
val requestsForUser = pendingRequests[userId]
val requestsForUser = pendingRequests[otherUserId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[userId] = it
pendingRequests[otherUserId] = it
}
val transport = sasTransportRoomMessageFactory.createTransport(roomId, null)
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
// Cancel existing pending requests?
requestsForUser.toImmutableList().forEach { existingRequest ->
@ -744,10 +875,24 @@ internal class DefaultVerificationService @Inject constructor(
isIncoming = false,
roomId = roomId,
localID = localID,
otherUserId = userId
otherUserId = otherUserId
)
transport.sendVerificationRequest(methods.map { it.toValue() }, localID, userId, roomId) { syncedId, info ->
// We can SCAN or SHOW QR codes only if cross-signing is enabled
val methodValues = if (crossSigningService.isCrossSigningEnabled()) {
// Add reciprocate method if application declares it can scan or show QR codes
// Not sure if it ok to do that (?)
val reciprocateMethod = methods.firstOrNull { it == VerificationMethod.QR_CODE_SCAN || it == VerificationMethod.QR_CODE_SHOW }?.let { listOf(VERIFICATION_METHOD_RECIPROCATE) }.orEmpty()
methods.map { it.toValue() } + reciprocateMethod
} else {
// Filter out SCAN and SHOW qr code method
methods
.filter { it != VerificationMethod.QR_CODE_SHOW && it != VerificationMethod.QR_CODE_SCAN }
.map { it.toValue() }
}
.distinct()
transport.sendVerificationRequest(methodValues, localID, otherUserId, roomId) { syncedId, info ->
// We need to update with the syncedID
updatePendingRequest(verificationRequest.copy(
transactionId = syncedId,
@ -762,7 +907,7 @@ internal class DefaultVerificationService @Inject constructor(
}
override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) {
sasTransportRoomMessageFactory.createTransport(roomId, null)
verificationTransportRoomMessageFactory.createTransport(roomId, null)
.cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User)
getExistingVerificationRequest(otherUserId, transactionId)?.let {
@ -795,42 +940,52 @@ internal class DefaultVerificationService @Inject constructor(
otherDeviceId: String,
callback: MatrixCallback<String>?): String? {
if (method == VerificationMethod.SAS) {
val tx = DefaultOutgoingSASDefaultVerificationRequest(
val tx = DefaultOutgoingSASDefaultVerificationTransaction(
setDeviceVerificationAction,
credentials,
userId,
deviceId,
cryptoStore,
crossSigningService,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
transactionId,
otherUserId,
otherDeviceId)
tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, tx)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
addTransaction(tx)
tx.start(method)
tx.start()
return transactionId
} else {
throw IllegalArgumentException("Unknown verification method")
}
}
override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean {
override fun readyPendingVerificationInDMs(methods: List<VerificationMethod>,
otherUserId: String,
roomId: String,
transactionId: String): Boolean {
Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId")
// Let's find the related request
val existingRequest = getExistingVerificationRequest(otherUserId, transactionId)
if (existingRequest != null) {
// we need to send a ready event, with matching methods
val transport = sasTransportRoomMessageFactory.createTransport(roomId, null)
// TODO We should not use supportedVerificationMethods here, because it depends on the client implementation
val methods = existingRequest.requestInfo?.methods?.intersect(supportedVerificationMethods)?.toList()
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
val computedMethods = computeReadyMethods(
transactionId,
otherUserId,
existingRequest.requestInfo?.fromDevice ?: "",
roomId,
existingRequest.requestInfo?.methods,
methods)
if (methods.isNullOrEmpty()) {
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
// TODO buttons should not be shown in this case?
return false
}
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods)
transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg,
val readyMsg = transport.createReady(transactionId, deviceId ?: "", computedMethods)
transport.sendToOther(EventType.KEY_VERIFICATION_READY,
readyMsg,
VerificationTxState.None,
CancelCode.User,
null // TODO handle error?
@ -844,15 +999,73 @@ internal class DefaultVerificationService @Inject constructor(
}
}
private fun computeReadyMethods(
transactionId: String,
otherUserId: String,
otherDeviceId: String,
roomId: String,
otherUserMethods: List<String>?,
methods: List<VerificationMethod>): List<String> {
if (otherUserMethods.isNullOrEmpty()) {
return emptyList()
}
val result = mutableSetOf<String>()
if (VERIFICATION_METHOD_SAS in otherUserMethods && VerificationMethod.SAS in methods) {
// Other can do SAS and so do I
result.add(VERIFICATION_METHOD_SAS)
}
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods || VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods) {
// Other user wants to verify using QR code. Cross-signing has to be setup
val qrCodeData = createQrCodeData(transactionId, otherUserId)
if (qrCodeData != null) {
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) {
// Other can Scan and I can show QR code
result.add(VERIFICATION_METHOD_QR_CODE_SHOW)
result.add(VERIFICATION_METHOD_RECIPROCATE)
}
if (VERIFICATION_METHOD_QR_CODE_SHOW in otherUserMethods && VerificationMethod.QR_CODE_SCAN in methods) {
// Other can show and I can scan QR code
result.add(VERIFICATION_METHOD_QR_CODE_SCAN)
result.add(VERIFICATION_METHOD_RECIPROCATE)
}
}
if (VERIFICATION_METHOD_RECIPROCATE in result) {
// Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
transactionId,
otherUserId,
otherDeviceId,
crossSigningService,
cryptoStore,
qrCodeData,
userId,
deviceId ?: "",
false)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
addTransaction(tx)
}
}
return result.toList()
}
/**
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/
private fun createUniqueIDForTransaction(userId: String, deviceID: String): String {
private fun createUniqueIDForTransaction(otherUserId: String, otherDeviceID: String): String {
return buildString {
append(credentials.userId).append("|")
append(credentials.deviceId).append("|")
append(userId).append("|")
append(deviceID).append("|")
append(deviceId).append("|")
append(otherUserId).append("|")
append(otherDeviceID).append("|")
append(UUID.randomUUID().toString())
}
}
@ -867,5 +1080,13 @@ internal class DefaultVerificationService @Inject constructor(
// remove
this.removeTransaction(tx.otherUserId, tx.transactionId)
}
if (tx is QrCodeVerificationTransaction
&& (tx.state == VerificationTxState.Cancelled
|| tx.state == VerificationTxState.OnCancelled
|| tx.state == VerificationTxState.Verified)
) {
// remove
this.removeTransaction(tx.otherUserId, tx.transactionId)
}
}
}

View file

@ -25,6 +25,7 @@ import java.util.UUID
/**
* Stores current pending verification requests
* TODO We should not expose this whole object to the app. Create an interface
*/
data class PendingVerificationRequest(
val ageLocalTs: Long,
@ -38,7 +39,6 @@ data class PendingVerificationRequest(
val cancelConclusion: CancelCode? = null,
val isSuccessful: Boolean = false,
val handledByOtherSession: Boolean = false
) {
val isReady: Boolean = readyInfo != null
val isSent: Boolean = transactionId != null

View file

@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.crypto.verification
import android.os.Build
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
@ -31,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import im.vector.matrix.android.internal.util.withoutPrefix
import org.matrix.olm.OlmSAS
import org.matrix.olm.OlmUtility
import timber.log.Timber
@ -41,15 +41,16 @@ import kotlin.properties.Delegates
*/
internal abstract class SASDefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
open val credentials: Credentials,
open val userId: String,
open val deviceId: String?,
private val cryptoStore: IMXCryptoStore,
private val crossSigningService: CrossSigningService,
private val deviceFingerprint: String,
transactionId: String,
otherUserId: String,
otherDevice: String?,
isIncoming: Boolean) :
DefaultVerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming), SasVerificationTransaction {
otherDeviceId: String?,
isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction {
companion object {
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
@ -139,11 +140,7 @@ internal abstract class SASDefaultVerificationTransaction(
// - the device ID of the device receiving the MAC,
// - the transaction ID, and
// - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs.
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
credentials.userId + credentials.deviceId +
otherUserId + otherDeviceId +
transactionId
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// Previously, with SAS verification, the m.key.verification.mac message only contained the user's device key.
// It should now contain both the device key and the MSK.
@ -151,7 +148,7 @@ internal abstract class SASDefaultVerificationTransaction(
val keyMap = HashMap<String, String>()
val keyId = "ed25519:${credentials.deviceId}"
val keyId = "ed25519:$deviceId"
val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId)
if (macString.isNullOrBlank()) {
@ -211,7 +208,7 @@ internal abstract class SASDefaultVerificationTransaction(
when (info) {
is VerificationInfoStart -> onVerificationStart(info)
is VerificationInfoAccept -> onVerificationAccept(info)
is VerificationInfoKey -> onKeyVerificationKey(senderId, info)
is VerificationInfoKey -> onKeyVerificationKey(info)
is VerificationInfoMac -> onKeyVerificationMac(info)
else -> {
// nop
@ -223,7 +220,7 @@ internal abstract class SASDefaultVerificationTransaction(
abstract fun onVerificationAccept(accept: VerificationInfoAccept)
abstract fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey)
abstract fun onKeyVerificationKey(vKey: VerificationInfoKey)
abstract fun onKeyVerificationMac(vKey: VerificationInfoMac)
@ -241,7 +238,7 @@ internal abstract class SASDefaultVerificationTransaction(
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
otherUserId + otherDeviceId +
credentials.userId + credentials.deviceId +
userId + deviceId +
transactionId
val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",")
@ -257,7 +254,7 @@ internal abstract class SASDefaultVerificationTransaction(
// cannot be empty because it has been validated
theirMac!!.mac!!.keys.forEach {
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
if (otherDeviceKey == null) {
Timber.w("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
@ -280,7 +277,7 @@ internal abstract class SASDefaultVerificationTransaction(
if (otherCrossSigningMasterKeyPublic != null) {
// Did the user signed his master key
theirMac!!.mac!!.keys.forEach {
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
// Check the signature
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
@ -305,7 +302,7 @@ internal abstract class SASDefaultVerificationTransaction(
}
// If not me sign his MSK and upload the signature
if (otherMasterKeyIsVerified && otherUserId != credentials.userId) {
if (otherMasterKeyIsVerified && otherUserId != userId) {
// we should trust this master key
// And check verification MSK -> SSK?
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
@ -315,9 +312,9 @@ internal abstract class SASDefaultVerificationTransaction(
})
}
if (otherUserId == credentials.userId) {
if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may ot be able to do it
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId")

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoStart : VerificationInfo {
val method: String?
/**
* Alices device ID
*/
@ -58,5 +59,10 @@ internal interface VerificationInfoStart : VerificationInfo {
*/
val shortAuthenticationStrings: List<String>?
/**
* Shared secret, when starting verification with QR code
*/
val sharedSecret: String?
fun toCanonicalJson(): String?
}

View file

@ -20,8 +20,8 @@ import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
/**
* SAS verification can be performed using toDevice events or via DM.
* This class abstracts the concept of transport for SAS
* Verification can be performed using toDevice events or via DM.
* This class abstracts the concept of transport for verification
*/
internal interface VerificationTransport {
@ -46,6 +46,7 @@ internal interface VerificationTransport {
code: CancelCode)
fun done(transactionId: String)
/**
* Creates an accept message suitable for this transport
*/
@ -59,14 +60,23 @@ internal interface VerificationTransport {
fun createKey(tid: String,
pubKey: String): VerificationInfoKey
fun createStart(fromDevice: String,
method: String,
/**
* Create start for SAS verification
*/
fun createStartForSas(fromDevice: String,
transactionID: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
messageAuthenticationCodes: List<String>,
shortAuthenticationStrings: List<String>): VerificationInfoStart
/**
* Create start for QR code verification
*/
fun createStartForQrCode(fromDevice: String,
transactionID: String,
sharedSecret: String): VerificationInfoStart
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady

View file

@ -16,14 +16,33 @@
package im.vector.matrix.android.internal.crypto.verification
import androidx.lifecycle.Observer
import androidx.work.*
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.Operation
import androidx.work.WorkInfo
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.events.model.*
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationAcceptContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationDoneContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationKeyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationMacContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserId
@ -35,7 +54,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -245,8 +264,7 @@ internal class VerificationTransportRoomMessage(
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys)
override fun createStart(fromDevice: String,
method: String,
override fun createStartForSas(fromDevice: String,
transactionID: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
@ -258,11 +276,30 @@ internal class VerificationTransportRoomMessage(
keyAgreementProtocols,
messageAuthenticationCodes,
shortAuthenticationStrings,
method,
VERIFICATION_METHOD_SAS,
RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = transactionID
),
null
)
}
override fun createStartForQrCode(fromDevice: String,
transactionID: String,
sharedSecret: String): VerificationInfoStart {
return MessageVerificationStartContent(
fromDevice,
null,
null,
null,
null,
VERIFICATION_METHOD_RECIPROCATE,
RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = transactionID
),
sharedSecret
)
}
@ -292,7 +329,7 @@ internal class VerificationTransportRoomMessage(
}
}
internal class SasTransportRoomMessageFactory @Inject constructor(
internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
private val stringProvider: StringProvider,
private val monarchy: Monarchy,

View file

@ -21,7 +21,14 @@ import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -119,8 +126,7 @@ internal class VerificationTransportToDevice(
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys)
override fun createStart(fromDevice: String,
method: String,
override fun createStartForSas(fromDevice: String,
transactionID: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
@ -128,12 +134,27 @@ internal class VerificationTransportToDevice(
shortAuthenticationStrings: List<String>): VerificationInfoStart {
return KeyVerificationStart(
fromDevice,
method,
VERIFICATION_METHOD_SAS,
transactionID,
keyAgreementProtocols,
hashes,
messageAuthenticationCodes,
shortAuthenticationStrings)
shortAuthenticationStrings,
null)
}
override fun createStartForQrCode(fromDevice: String,
transactionID: String,
sharedSecret: String): VerificationInfoStart {
return KeyVerificationStart(
fromDevice,
VERIFICATION_METHOD_RECIPROCATE,
transactionID,
null,
null,
null,
null,
sharedSecret)
}
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
@ -145,7 +166,7 @@ internal class VerificationTransportToDevice(
}
}
internal class SasTransportToDeviceFactory @Inject constructor(
internal class VerificationTransportToDeviceFactory @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor) {

View file

@ -0,0 +1,247 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.rest.SignatureUploadResponse
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.withoutPrefix
import timber.log.Timber
import kotlin.properties.Delegates
internal class DefaultQrCodeVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
override val transactionId: String,
override val otherUserId: String,
override var otherDeviceId: String?,
private val crossSigningService: CrossSigningService,
private val cryptoStore: IMXCryptoStore,
// Not null only if other user is able to scan QR code
private val qrCodeData: QrCodeData?,
val userId: String,
val deviceId: String,
override val isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
override var cancelledReason: CancelCode? = null
override val qrCodeText: String?
get() = qrCodeData?.toUrl()
override var state by Delegates.observable(VerificationTxState.None) { _, _, _ ->
listeners.forEach {
try {
it.transactionUpdated(this)
} catch (e: Throwable) {
Timber.e(e, "## Error while notifying listeners")
}
}
}
override fun userHasScannedOtherQrCode(otherQrCodeText: String) {
val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run {
Timber.d("## Verification QR: Invalid QR Code Data")
cancel(CancelCode.QrCodeInvalid)
return
}
// Perform some checks
if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) {
Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}")
cancel(CancelCode.QrCodeInvalid)
return
}
if (otherQrCodeData.userId != otherUserId) {
Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}")
cancel(CancelCode.MismatchedUser)
return
}
if (otherQrCodeData.requestEventId != transactionId) {
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestEventId} expected:$transactionId")
cancel(CancelCode.QrCodeInvalid)
return
}
// check master key
if (otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}")
cancel(CancelCode.MismatchedKeys)
return
}
val toVerifyDeviceIds = mutableListOf<String>()
var canTrustOtherUserMasterKey = false
val otherDevices = cryptoStore.getUserDevices(otherUserId)
otherQrCodeData.keys.keys.forEach { key ->
Timber.w("## Verification QR: Checking key $key")
when (val keyNoPrefix = key.withoutPrefix("ed25519:")) {
otherQrCodeData.keys[key] -> {
// Maybe master key?
if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
canTrustOtherUserMasterKey = true
} else {
cancel(CancelCode.MismatchedKeys)
return
}
}
else -> {
when (val otherDevice = otherDevices?.get(keyNoPrefix)) {
null -> {
// Unknown device, ignore
}
else -> {
when (otherDevice.fingerprint()) {
null -> {
// Ignore
}
otherQrCodeData.keys[key] -> {
// Store the deviceId to verify after
toVerifyDeviceIds.add(key)
}
else -> {
cancel(CancelCode.MismatchedKeys)
return
}
}
}
}
}
}
}
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// Nothing to verify
cancel(CancelCode.MismatchedKeys)
return
}
// All checks are correct
// Send the shared secret so that sender can trust me
// qrCodeData.sharedSecret will be used to send the start request
start(otherQrCodeData.sharedSecret)
// Trust the other user
trust(canTrustOtherUserMasterKey, toVerifyDeviceIds)
}
fun start(remoteSecret: String) {
if (state != VerificationTxState.None) {
Timber.e("## Verification QR: start verification from invalid state")
// should I cancel??
throw IllegalStateException("Interactive Key verification already started")
}
val startMessage = transport.createStartForQrCode(
deviceId,
transactionId,
remoteSecret
)
transport.sendToOther(
EventType.KEY_VERIFICATION_START,
startMessage,
VerificationTxState.Started,
CancelCode.User,
null
)
}
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
}
override fun cancel() {
cancel(CancelCode.User)
}
override fun cancel(code: CancelCode) {
cancelledReason = code
state = VerificationTxState.Cancelled
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
}
override fun isToDeviceTransport() = false
// Other user has scanned our QR code. check that the secret matched, so we can trust him
fun onStartReceived(startReq: VerificationInfoStart) {
if (qrCodeData == null) {
// Should not happen
cancel(CancelCode.UnexpectedMessage)
return
}
if (startReq.sharedSecret == qrCodeData.sharedSecret) {
// Ok, we can trust the other user
// We can only trust the master key in this case
trust(true, emptyList())
} else {
// Display a warning
cancel(CancelCode.MismatchedKeys)
}
}
private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List<String>) {
// If not me sign his MSK and upload the signature
if (otherUserId != userId && canTrustOtherUserMasterKey) {
// we should trust this master key
// And check verification MSK -> SSK?
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
}
})
}
if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.signDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId")
}
})
}
// TODO what if the otherDevice is not in this list? and should we
toVerifyDeviceIds.forEach {
setDeviceVerified(otherUserId, it)
}
transport.done(transactionId)
state = VerificationTxState.Verified
}
private fun setDeviceVerified(userId: String, deviceId: String) {
// TODO should not override cross sign status
setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
userId,
deviceId)
}
}

View file

@ -18,6 +18,10 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import java.net.URLDecoder
import java.net.URLEncoder
private const val ENCODING = "utf-8"
/**
* Generate an URL to generate a QR code of the form:
@ -28,27 +32,34 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
* &key_<keyid>=<key-in-base64>...
* &secret=<shared_secret>
* &other_user_key=<master-key-in-base64>
*
* Example:
* https://matrix.to/#/@user:matrix.org?
* request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
* &action=verify
* &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
* &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
* &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
* &other_user_key=WqSVLkBCS%2Fi5NqR%2F%2FymC8T7K9RPxBIuqK8Usl6Y3big
* </pre>
*/
fun QrCodeData.toUrl(): String {
return buildString {
append(PermalinkFactory.createPermalink(userId))
append("?request=")
append(PermalinkFactory.escape(requestEventId))
append(URLEncoder.encode(requestEventId, ENCODING))
append("&action=")
append(PermalinkFactory.escape(action))
append(URLEncoder.encode(action, ENCODING))
for ((keyId, key) in keys) {
append("&key_")
append(PermalinkFactory.escape(keyId))
append("=")
append(PermalinkFactory.escape(key))
append("&key_$keyId=")
append(URLEncoder.encode(key, ENCODING))
}
append("&secret=")
append(PermalinkFactory.escape(sharedSecret))
append(URLEncoder.encode(sharedSecret, ENCODING))
append("&other_user_key=")
append(PermalinkFactory.escape(otherUserKey))
append(URLEncoder.encode(otherUserKey, ENCODING))
}
}
@ -82,7 +93,7 @@ fun String.toQrCodeData(): QrCodeData? {
.filter { it.isNotEmpty() }
val keyValues = urlParams.map {
(it.substringBefore("=") to it.substringAfter("=").let { value -> PermalinkFactory.unescape(value) })
(it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) })
}.toMap()
val action = keyValues["action"] ?: return null

View file

@ -49,3 +49,5 @@ fun convertFromUTF8(s: String): String {
s
}
}
fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this

View file

@ -39,7 +39,7 @@ class QrCodeTest {
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"
private val basicUrl = "https://matrix.to/#/@benoit:matrix.org?request=%24azertyazerty&action=verify&key_1=abcdef&key_2=ghijql&secret=sharedSecret&other_user_key=otherUserKey"
@Test
fun testNominalCase() {
@ -101,7 +101,7 @@ class QrCodeTest {
@Test
fun testBadRequestEventId() {
basicUrl.replace("\$azertyazerty", "@azertyazerty")
basicUrl.replace("%24azertyazerty", "%32azertyazerty")
.toQrCodeData()
.shouldBeNull()
}

View file

@ -29,7 +29,6 @@ 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
@ -56,8 +55,7 @@ class DebugMenuActivity : VectorBaseActivity() {
}
private fun renderQrCode(text: String) {
val qrBitmap = text.toQrCode(200, 200)
debug_qr_code.setImageBitmap(qrBitmap)
debug_qr_code.setData(text, true)
}
@OnClick(R.id.debug_test_text_view_link)

View file

@ -68,7 +68,7 @@
android:layout_height="wrap_content"
android:text="Scan QR-code" />
<ImageView
<im.vector.riotx.core.ui.views.QrCodeImageView
android:id="@+id/debug_qr_code"
android:layout_width="200dp"
android:layout_height="200dp"

View file

@ -23,22 +23,17 @@ 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 {
fun String.toBitMatrix(size: Int): BitMatrix {
return QRCodeWriter().encode(
this,
BarcodeFormat.QR_CODE,
width,
height
).toBitmap(backgroundColor, foregroundColor)
size,
size
)
}
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) {

View file

@ -0,0 +1,99 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.ui.views
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import im.vector.riotx.core.qrcode.toBitMatrix
import im.vector.riotx.core.qrcode.toBitmap
import kotlin.random.Random
class QrCodeImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
private var data: String? = null
private var animate = false
init {
setBackgroundColor(Color.WHITE)
}
fun setData(data: String, animate: Boolean) {
this.data = data
this.animate = animate
render()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
render()
}
private fun render() {
data
?.takeIf { height > 0 }
?.let {
if (animate) {
// NOT SUPPORTED YET val anim = createAnimation(it)
// NOT SUPPORTED YET setImageDrawable(anim)
// NOT SUPPORTED YET anim.start()
// NOT SUPPORTED YET setImageDrawable(BitmapDrawable(resources, it.toBitMatrix(height).toBitmap()))
val bitmap = it.toBitMatrix(height).toBitmap()
post { setImageBitmap(bitmap) }
} else {
val bitmap = it.toBitMatrix(height).toBitmap()
post { setImageBitmap(bitmap) }
}
}
}
private fun createAnimation(data: String): AnimationDrawable {
val finalQr = data.toBitMatrix(height)
val list = mutableListOf(finalQr)
val random = Random(System.currentTimeMillis())
val repeatTime = 8
repeat(repeatTime) { index ->
val alteredQr = finalQr.clone()
for (x in 0 until alteredQr.width) {
for (y in 0 until alteredQr.height) {
if (random.nextInt(repeatTime - index) == 0) {
// Pb is that it does not toggle a whole black square, but only a pixel
alteredQr.unset(x, y)
}
}
}
list.add(alteredQr)
}
val animDrawable = AnimationDrawable()
list.asReversed()
.forEach {
animDrawable.addFrame(BitmapDrawable(resources, it.toBitmap()), 150)
}
return animDrawable
}
}

View file

@ -64,12 +64,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
fun start(session: Session) {
this.session = session
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
session.addRoomKeysRequestListener(this)
}
fun stop() {
session?.getSasVerificationService()?.removeListener(this)
session?.getVerificationService()?.removeListener(this)
session?.removeRoomKeysRequestListener(this)
session = null
}

View file

@ -40,11 +40,11 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
fun start(session: Session) {
this.session = session
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
}
fun stop() {
session?.getSasVerificationService()?.removeListener(this)
session?.getVerificationService()?.removeListener(this)
this.session = null
}
@ -130,7 +130,7 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context
}
}
dismissedAction = Runnable {
session?.getSasVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
session?.getVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId,
pr.requestInfo?.fromDevice ?: "",
pr.transactionId ?: "",
pr.roomId ?: ""

View file

@ -19,10 +19,10 @@ 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 transactionId: 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()
data class RequestVerificationByDM(val otherUserId: String, val roomId: String?) : VerificationAction()
data class StartSASVerification(val otherUserId: String, val pendingRequestTransactionId: String) : VerificationAction()
data class RemoteQrCodeScanned(val otherUserId: String, val transactionId: String, val scannedData: String) : VerificationAction()
data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
object GotItConclusion : VerificationAction()
}

View file

@ -98,7 +98,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
it.otherUserMxItem?.let { matrixItem ->
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
if (it.transactionState == VerificationTxState.Verified) {
if (it.sasTransactionState == VerificationTxState.Verified || it.qrTransactionState == VerificationTxState.Verified) {
otherUserNameText.text = getString(R.string.verification_verified_user, matrixItem.getBestName())
otherUserShield.isVisible = true
} else {
@ -108,8 +108,8 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
// Did the request result in a SAS transaction?
if (it.transactionState != null) {
when (it.transactionState) {
if (it.sasTransactionState != null) {
when (it.sasTransactionState) {
VerificationTxState.None,
VerificationTxState.SendingStart,
VerificationTxState.Started,
@ -138,7 +138,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
VerificationTxState.OnCancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
it.transactionState == VerificationTxState.Verified,
it.sasTransactionState == VerificationTxState.Verified,
it.cancelCode?.value))
})
}
@ -147,7 +147,21 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
return@withState
}
// At this point there is no transaction for this request
when (it.qrTransactionState) {
VerificationTxState.Verified,
VerificationTxState.Cancelled,
VerificationTxState.OnCancelled -> {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
it.qrTransactionState == VerificationTxState.Verified,
it.cancelCode?.value))
})
return@withState
}
else -> Unit
}
// At this point there is no SAS transaction for this request
// Transaction has not yet started
if (it.pendingRequest.invoke()?.cancelConclusion != null) {

View file

@ -31,7 +31,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.QRVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
@ -46,13 +46,15 @@ import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null,
val roomId: String? = null,
val pendingRequest: Async<PendingVerificationRequest> = Uninitialized,
val pendingLocalId: String? = null,
val transactionState: VerificationTxState? = null,
val sasTransactionState: VerificationTxState? = null,
val qrTransactionState: VerificationTxState? = null,
val transactionId: String? = null,
val cancelCode: CancelCode? = null
) : MvRxState
@ -68,11 +70,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
get() = _requestLiveData
init {
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
session.getVerificationService().removeListener(this)
super.onCleared()
}
@ -91,15 +93,20 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
val userItem = session.getUser(args.otherUserId)
val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
val pr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
val sasTx = (pr?.transactionId ?: args.verificationId)?.let {
session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? SasVerificationTransaction
}
val qrTx = (pr?.transactionId ?: args.verificationId)?.let {
session.getVerificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
otherUserMxItem = userItem?.toMatrixItem(),
transactionState = sasTx?.state,
sasTransactionState = sasTx?.state,
qrTransactionState = qrTx?.state,
transactionId = args.verificationId,
pendingRequest = if (pr != null) Success(pr) else Uninitialized,
roomId = args.roomId)
@ -132,7 +139,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
copy(
roomId = data,
pendingRequest = Success(
session.getSasVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, data, pendingLocalId)
session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, data, pendingLocalId)
)
)
}
@ -146,16 +153,16 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
})
} else {
setState {
copy(pendingRequest = Success(session.getSasVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, roomId)))
copy(pendingRequest = Success(session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, otherUserId, roomId)))
}
}
}
is VerificationAction.StartSASVerification -> {
val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
val request = session.getVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
?: return@withState
if (roomId == null) return@withState
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
session.getSasVerificationService().beginKeyVerificationInDMs(
session.getVerificationService().beginKeyVerificationInDMs(
VerificationMethod.SAS,
transactionId = action.pendingRequestTransactionId,
roomId = roomId,
@ -165,20 +172,24 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
)
}
is VerificationAction.RemoteQrCodeScanned -> {
// TODO Use session.getCrossSigningService()?
val existingTransaction = session.getSasVerificationService()
.getExistingTransaction(action.userID, action.transactionId) as? QRVerificationTransaction
val existingTransaction = session.getVerificationService()
.getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
existingTransaction
?.userHasScannedRemoteQrCode(action.scannedData)
?.userHasScannedOtherQrCode(action.scannedData)
?.let { cancelCode ->
// Something went wrong
Timber.w("## Something is not right: $cancelCode")
// TODO
}
}
is VerificationAction.SASMatchAction -> {
(session.getSasVerificationService()
.getExistingTransaction(action.userID, action.sasTransactionId)
(session.getVerificationService()
.getExistingTransaction(action.otherUserId, action.sasTransactionId)
as? SasVerificationTransaction)?.userHasVerifiedShortCode()
}
is VerificationAction.SASDoNotMatchAction -> {
(session.getSasVerificationService()
.getExistingTransaction(action.userID, action.sasTransactionId)
(session.getVerificationService()
.getExistingTransaction(action.otherUserId, action.sasTransactionId)
as? SasVerificationTransaction)
?.shortCodeDoesNotMatch()
}
@ -193,16 +204,31 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
}
override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
when (tx) {
is SasVerificationTransaction -> {
if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
// A SAS tx has been started following this request
setState {
copy(
transactionState = tx.state,
sasTransactionState = tx.state,
cancelCode = tx.cancelledReason
)
}
}
}
is QrCodeVerificationTransaction -> {
if (tx.transactionId == (state.pendingRequest.invoke()?.transactionId ?: state.transactionId)) {
// A SAS tx has been started following this request
setState {
copy(
qrTransactionState = tx.state,
cancelCode = tx.cancelledReason
)
}
}
}
}
}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
verificationRequestUpdated(pr)

View file

@ -19,19 +19,16 @@ 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
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem
import javax.inject.Inject
class VerificationChooseMethodController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter
private val colorProvider: ColorProvider
) : EpoxyController() {
var listener: Listener? = null
@ -52,14 +49,11 @@ class VerificationChooseMethodController @Inject constructor(
notice(stringProvider.getString(R.string.verification_scan_notice))
}
if (state.otherCanScanQrCode && !state.QRtext.isNullOrBlank()) {
// Generate the QR code
val size = dimensionConverter.dpToPx(180)
val qrCodeBitmap = state.QRtext.toQrCode(size, size)
bottomSheetVerificationBigImageItem {
if (state.otherCanScanQrCode && !state.qrCodeText.isNullOrBlank()) {
bottomSheetVerificationQrCodeItem {
id("qr")
imageBitmap(qrCodeBitmap)
data(state.qrCodeText)
animate(false)
}
dividerItem {

View file

@ -69,10 +69,10 @@ class VerificationChooseMethodFragment @Inject constructor(
controller.update(state)
}
override fun doVerifyBySas() = withState(sharedViewModel) {
override fun doVerifyBySas() = withState(sharedViewModel) { state ->
sharedViewModel.handle(VerificationAction.StartSASVerification(
it.otherUserMxItem?.id ?: "",
it.pendingRequest.invoke()?.transactionId ?: ""))
state.otherUserMxItem?.id ?: "",
state.pendingRequest.invoke()?.transactionId ?: ""))
}
override fun openCamera() {
@ -112,10 +112,10 @@ class VerificationChooseMethodFragment @Inject constructor(
}
}
private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) {
private fun onRemoteQrCodeScanned(remoteQrCode: String) = withState(sharedViewModel) { state ->
sharedViewModel.handle(VerificationAction.RemoteQrCodeScanned(
it.otherUserMxItem?.id ?: "",
it.pendingRequest.invoke()?.transactionId ?: "",
state.otherUserMxItem?.id ?: "",
state.pendingRequest.invoke()?.transactionId ?: "",
remoteQrCode
))
}

View file

@ -22,8 +22,9 @@ 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.VerificationService
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.di.HasScreenInjector
@ -37,7 +38,7 @@ data class VerificationChooseMethodViewState(
val transactionId: String = "",
val otherCanShowQrCode: Boolean = false,
val otherCanScanQrCode: Boolean = false,
val QRtext: String? = null,
val qrCodeText: String? = null,
val SASModeAvailable: Boolean = false
) : MvRxState
@ -46,19 +47,27 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
private val session: Session
) : VectorViewModel<VerificationChooseMethodViewState, EmptyAction, EmptyViewEvents>(initialState), VerificationService.VerificationListener {
override fun transactionCreated(tx: VerificationTransaction) {}
override fun transactionCreated(tx: VerificationTransaction) {
transactionUpdated(tx)
}
override fun transactionUpdated(tx: VerificationTransaction) {}
override fun transactionUpdated(tx: VerificationTransaction) = withState { state ->
if (tx.transactionId == state.transactionId && tx is QrCodeVerificationTransaction) {
setState {
copy(
qrCodeText = tx.qrCodeText
)
}
}
}
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
val pvr = session.getVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
setState {
copy(
otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
// TODO
QRtext = "https://www.example.org",
SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
)
}
@ -70,12 +79,12 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
}
init {
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
}
override fun onCleared() {
super.onCleared()
session.getSasVerificationService().removeListener(this)
session.getVerificationService().removeListener(this)
}
companion object : MvRxViewModelFactory<VerificationChooseMethodViewModel, VerificationChooseMethodViewState> {
@ -87,14 +96,16 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
val pvr = session.getVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
// Get the QR code now, because transaction is already created, so transactionCreated() will not be called
val qrCodeVerificationTransaction = session.getVerificationService().getExistingTransaction(args.otherUserId, args.verificationId ?: "")
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
transactionId = args.verificationId ?: "",
otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
// TODO
QRtext = "https://www.example.org",
qrCodeText = (qrCodeVerificationTransaction as? QrCodeVerificationTransaction)?.qrCodeText,
SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
)
}

View file

@ -43,6 +43,8 @@ class VerificationConclusionViewModel(initialState: VerificationConclusionViewSt
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
return when (safeValueOf(args.cancelReason)) {
CancelCode.QrCodeInvalid,
CancelCode.MismatchedUser,
CancelCode.MismatchedSas,
CancelCode.MismatchedCommitment,
CancelCode.MismatchedKeys -> {

View file

@ -48,16 +48,16 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor(
init {
withState { state ->
refreshStateFromTx(session.getSasVerificationService()
refreshStateFromTx(session.getVerificationService()
.getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
?: "") as? SasVerificationTransaction)
}
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
session.getVerificationService().removeListener(this)
super.onCleared()
}

View file

@ -16,7 +16,6 @@
*/
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
@ -34,18 +33,11 @@ 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

@ -0,0 +1,45 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.riotx.features.crypto.verification.epoxy
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.ui.views.QrCodeImageView
/**
* An Epoxy item displaying a QR code
*/
@EpoxyModelClass(layout = R.layout.item_verification_qr_code)
abstract class BottomSheetVerificationQrCodeItem : VectorEpoxyModel<BottomSheetVerificationQrCodeItem.Holder>() {
@EpoxyAttribute
lateinit var data: String
@EpoxyAttribute
var animate = false
override fun bind(holder: Holder) {
holder.qsrCodeImage.setData(data, animate)
}
class Holder : VectorEpoxyHolder() {
val qsrCodeImage by bind<QrCodeImageView>(R.id.itemVerificationQrCodeImage)
}
}

View file

@ -411,7 +411,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
popDraft()
}
is ParsedCommand.VerifyUser -> {
session.getSasVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
popDraft()
}
@ -809,14 +809,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
if (session.getVerificationService().readyPendingVerificationInDMs(
supportedVerificationMethods,
action.otherUserId,
room.roomId,
action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action)))
} else {
// TODO
}
}
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
session.getSasVerificationService().declineVerificationRequestInDMs(
session.getVerificationService().declineVerificationRequestInDMs(
action.otherUserId,
action.otherdDeviceId,
action.transactionId,
@ -830,7 +835,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
// Check if this request is still active and handled by me
session.getSasVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
if (it.handledByOtherSession) return
if (!it.isFinished) {
_requestLiveData.postValue(LiveEvent(Success(action.copy(

View file

@ -65,7 +65,7 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransationId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
val tx = session.getSasVerificationService().getExistingTransaction(otherUserId, sasTransationId) ?: return
val tx = session.getVerificationService().getExistingTransaction(otherUserId, sasTransationId) ?: return
(tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(

View file

@ -55,7 +55,7 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
is VerificationAction.StartSASVerification -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.userID,
otherUserId = action.otherUserId,
transactionId = action.pendingRequestTransactionId
).show(requireActivity().supportFragmentManager, "REQPOP")
}

View file

@ -100,7 +100,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
fun manuallyVerify(device: CryptoDeviceInfo) {
session.getSasVerificationService().beginKeyVerification(VerificationMethod.SAS, deviceID = device.deviceId, userId = userId)?.let { txID ->
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId)?.let { txID ->
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
}
}

View file

@ -429,12 +429,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// TODO Move to a ViewModel...
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
if (isAdded) {
refreshCryptographyPreference(data.devices ?: emptyList())
}
}
override fun onFailure(failure: Throwable) {
if (isAdded) {
refreshCryptographyPreference(emptyList())
}
}
})
}

View file

@ -86,11 +86,11 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
init {
refreshDevicesList()
session.getSasVerificationService().addListener(this)
session.getVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
session.getVerificationService().removeListener(this)
super.onCleared()
}
@ -169,7 +169,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
// TODO Implement request in to DEVICE!!!
val txID = session.getSasVerificationService().beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId)
val txID = session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId)
if (txID != null) {
_requestLiveData.postValue(LiveEvent(Success(
action.copy(

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<im.vector.riotx.core.ui.views.QrCodeImageView
android:id="@+id/itemVerificationQrCodeImage"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/a11y_qr_code_for_verification"
tools:src="@color/riotx_header_panel_background_black" />
</FrameLayout>

View file

@ -100,7 +100,6 @@
<string name="room_settings_enable_encryption_dialog_content">Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly.</string>
<string name="room_settings_enable_encryption_dialog_submit">Enable encryption</string>
<string name="verification_request_notice">For extra security, verify %s by checking a one-time code.</string>
<string name="verification_request_start_notice">For maximum security, do this in person.</string>
@ -146,4 +145,6 @@
<string name="initialize_cross_signing">Initialize CrossSigning</string>
<string name="reset_cross_signing">Reset Keys</string>
<string name="a11y_qr_code_for_verification">QR code</string>
</resources>