QrCode: WIP

This commit is contained in:
Benoit Marty 2020-01-27 17:07:52 +01:00
parent 0aaba26f17
commit 39e746413a
12 changed files with 282 additions and 42 deletions

View file

@ -27,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 mismatch"),
UserMismatchError("m.user_mismatch", "Key mismatch"),
QrCodeInvalid("m.qr_code.invalid", "User mismatch")
}
fun safeValueOf(code: String?): CancelCode {

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 userHasScannedRemoteQrCode(otherQrCodeText: String): CancelCode?
}

View file

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

@ -24,6 +24,8 @@ interface VerificationTransaction {
val transactionId: String
val otherUserId: String
var otherDeviceId: String?
// TODO Not used. Remove?
val isIncoming: Boolean
/**
* User wants to cancel the transaction

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

@ -55,11 +55,12 @@ 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.supportedVerificationMethods
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.crypto.verification.qrcode.toUrl
@ -413,6 +414,16 @@ internal class DefaultVerificationService @Inject constructor(
autoAccept).also { txConfigure(it) }
addTransaction(tx)
tx.acceptVerificationEvent(otherUserId, startReq)
} else if (startReq.method == VERIFICATION_METHOD_RECIPROCATE) {
// Other user has scanned my QR code
val pendingTransaction = getExistingTransaction(otherUserId, startReq.transactionID!!)
if (pendingTransaction != null && pendingTransaction is DefaultQrCodeVerificationTransaction) {
pendingTransaction.onStartReceived(startReq)
} else {
Timber.w("## SAS onStartRequestReceived - unknown transaction ${startReq.transactionID}")
return CancelCode.UnknownTransaction
}
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
return CancelCode.UnknownMethod
@ -606,7 +617,7 @@ internal class DefaultVerificationService @Inject constructor(
return
}
handleReadyReceived(event.senderId, readyReq)
handleReadyReceived(event.senderId, event.roomId!!, readyReq)
}
private fun onRoomDoneReceived(event: Event) {
@ -651,7 +662,7 @@ 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}")
@ -710,10 +721,26 @@ internal class DefaultVerificationService @Inject constructor(
).toUrl()
}
if (qrCodeText != null) {
// Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction(
readyReq.transactionID!!,
senderId,
readyReq.fromDevice,
crossSigningService,
cryptoStore,
myGeneratedSharedSecret!!,
qrCodeText,
deviceId ?: "",
false)
tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
addTransaction(tx)
}
updatePendingRequest(existingRequest.copy(
readyInfo = readyReq,
myGeneratedSecret = myGeneratedSharedSecret,
qrCodeText = qrCodeText
readyInfo = readyReq
))
}
@ -916,23 +943,26 @@ internal class DefaultVerificationService @Inject constructor(
}
}
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 = verificationTransportRoomMessageFactory.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 computedMethods = computeReadyMethods(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, 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?
@ -946,6 +976,31 @@ internal class DefaultVerificationService @Inject constructor(
}
}
private fun computeReadyMethods(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 + VERIFICATION_METHOD_SAS
}
if (VERIFICATION_METHOD_QR_CODE_SCAN in otherUserMethods && VerificationMethod.QR_CODE_SHOW in methods) {
// Other can Scan and I can show QR code
result + VERIFICATION_METHOD_QR_CODE_SHOW
result + 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 + VERIFICATION_METHOD_QR_CODE_SCAN
result + VERIFICATION_METHOD_RECIPROCATE
}
return result.toList()
}
/**
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/

View file

@ -38,12 +38,7 @@ data class PendingVerificationRequest(
val readyInfo: VerificationInfoReady? = null,
val cancelConclusion: CancelCode? = null,
val isSuccessful: Boolean = false,
val handledByOtherSession: Boolean = false,
// TODO Move to OutgoingQrCodeTransaction
val myGeneratedSecret: String? = null,
// TODO Move to OutgoingQrCodeTransaction
val qrCodeText: String? = null
val handledByOtherSession: Boolean = false
) {
val isReady: Boolean = readyInfo != null
val isSent: Boolean = transactionId != null

View file

@ -0,0 +1,156 @@
/*
* 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.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 timber.log.Timber
import kotlin.properties.Delegates
internal class DefaultQrCodeVerificationTransaction(
override val transactionId: String,
override val otherUserId: String,
override var otherDeviceId: String?,
private val crossSigningService: CrossSigningService,
private val cryptoStore: IMXCryptoStore,
private val myGeneratedSecret: String,
override val qrCodeText: String,
val deviceId: String,
override val isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
override var cancelledReason: CancelCode? = null
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 userHasScannedRemoteQrCode(otherQrCodeText: String): CancelCode? {
val qrCodeData = otherQrCodeText.toQrCodeData() ?: return CancelCode.QrCodeInvalid
// Perform some checks
if (qrCodeData.action != QrCodeData.ACTION_VERIFY) {
return CancelCode.QrCodeInvalid
}
if (qrCodeData.userId != otherUserId) {
return CancelCode.UserMismatchError
}
if (qrCodeData.requestEventId != transactionId) {
return CancelCode.QrCodeInvalid
}
// check master key
if (qrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
return CancelCode.MismatchedKeys
}
val otherDevices = cryptoStore.getUserDevices(otherUserId)
qrCodeData.keys.keys.forEach { key ->
Timber.w("Checking key $key")
val fingerprint = otherDevices?.get(key)?.fingerprint()
if (fingerprint != null && fingerprint != qrCodeData.keys[key]) {
return CancelCode.MismatchedKeys
}
}
// All checks are correct
// Trust the other user
trust()
state = VerificationTxState.Verified
// Send the shared secret so that sender can trust me
// qrCodeData.sharedSecret will be used to send the start request
start(qrCodeData.sharedSecret)
return null
}
fun start(remoteSecret: String) {
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.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
// Remote user has scanned our QR code. check that the secret matched, so we can trust him
fun onStartReceived(startReq: VerificationInfoStart) {
if (startReq.sharedSecret == myGeneratedSecret) {
// Ok, we can trust the other user
trust()
} else {
// Display a warning
cancelledReason = CancelCode.QrCodeInvalid
state = VerificationTxState.OnCancelled
}
}
private fun trust() {
crossSigningService.trustUser(otherUserId, object : MatrixCallback<SignatureUploadResponse> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
}
})
}
}

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
@ -166,7 +166,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
}
is VerificationAction.RemoteQrCodeScanned -> {
val existingTransaction = session.getVerificationService()
.getExistingTransaction(action.otherUserId, action.transactionId) as? QRVerificationTransaction
.getExistingTransaction(action.otherUserId, action.transactionId) as? QrCodeVerificationTransaction
existingTransaction
?.userHasScannedRemoteQrCode(action.scannedData)
}

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
@ -46,9 +47,19 @@ 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.getVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
@ -57,7 +68,6 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
copy(
otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
qrCodeText = pvr?.qrCodeText,
SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
)
}
@ -92,7 +102,6 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
transactionId = args.verificationId ?: "",
otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false,
otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false,
qrCodeText = pvr?.qrCodeText,
SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false
)
}

View file

@ -809,7 +809,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
if (session.getVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
if (session.getVerificationService().readyPendingVerificationInDMs(
supportedVerificationMethods,
action.otherUserId,
room.roomId,
action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action)))
}