From a4e1a5bbcb12730421954697366e488bf7dac448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 17 Jun 2021 13:38:30 +0200 Subject: [PATCH] crypto: Initial support to answer to-device verification requests --- .../internal/crypto/DefaultCryptoService.kt | 44 +- .../android/sdk/internal/crypto/OlmMachine.kt | 567 +++++++++++------- .../verification/RustVerificationService.kt | 326 ++++++++++ 3 files changed, 724 insertions(+), 213 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index cf31327e18..37e1288525 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -86,7 +87,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask -import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.crypto.verification.RustVerificationService import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.SessionFilesDirectory @@ -131,9 +132,6 @@ internal class DefaultCryptoService @Inject constructor( private val mxCryptoConfig: MXCryptoConfig, // The key backup service. private val keysBackupService: DefaultKeysBackupService, - // The verification service. - private val verificationService: DefaultVerificationService, - private val crossSigningService: DefaultCrossSigningService, // Actions private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, @@ -156,6 +154,9 @@ internal class DefaultCryptoService @Inject constructor( private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) private var olmMachine: OlmMachine? = null + // The verification service. + private var verificationService: RustVerificationService? = null + private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver() // Locks for some of our operations @@ -179,6 +180,7 @@ internal class DefaultCryptoService @Inject constructor( EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + else -> this.verificationService?.onEvent(event) } } @@ -315,7 +317,10 @@ internal class DefaultCryptoService @Inject constructor( try { setRustLogger() - this.olmMachine = OlmMachine(userId, deviceId!!, dataDir, deviceObserver) + val machine = OlmMachine(userId, deviceId!!, dataDir, deviceObserver) + this.olmMachine = machine + this.verificationService = + RustVerificationService(this.taskExecutor, machine, this.sendToDeviceTask) Timber.v( "## CRYPTO | Successfully started up an Olm machine for " + "${userId}, ${deviceId}, identity keys: ${this.olmMachine?.identityKeys()}") @@ -359,7 +364,32 @@ internal class DefaultCryptoService @Inject constructor( /** * @return the VerificationService */ - override fun verificationService() = verificationService + override fun verificationService(): VerificationService { + // TODO yet another problem because the CryptoService is started in the + // sync loop + // + // The `KeyRequestHandler` and `IncomingVerificationHandler` want to add + // listeners to the verification service, they are initialized in the + // `ActiveSessionHolder` class in the `setActiveSession()` method. In + // the `setActiveSession()` method we call the `start()` method of the + // handlers without first calling the `start()` method of the + // `DefaultCrytpoService`. + // + // The start method of the crypto service isn't part of the + // `CryptoService` interface so it currently can't be called there. I'm + // inclined to believe that it should be, and that it should be + // initialized before anything else tries to do something with it. + // + // Let's initialize here as a workaround until we figure out if the + // above conclusion is correct. + if (verificationService == null) { + runBlocking { + internalStart() + } + } + + return verificationService!! + } override fun crossSigningService() = crossSigningService @@ -677,6 +707,8 @@ internal class DefaultCryptoService @Inject constructor( val sessionId = content.sessionId notifyRoomKeyReceival(roomId, sessionId) + } else { + this.verificationService?.onEvent(event) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index d62d880676..f1e21e0b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -26,33 +26,44 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.verification.CancelCode +import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoReady +import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE +import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import org.matrix.android.sdk.internal.crypto.verification.getEmojiForCode import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.session.sync.model.DeviceListResponse import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse import timber.log.Timber +import uniffi.olm.CancelCode as RustCancelCode import uniffi.olm.CryptoStoreErrorException import uniffi.olm.DecryptionErrorException -import uniffi.olm.Sas as InnerSas -import uniffi.olm.OutgoingVerificationRequest import uniffi.olm.Device import uniffi.olm.DeviceLists import uniffi.olm.KeyRequestPair import uniffi.olm.Logger import uniffi.olm.OlmMachine as InnerMachine +import uniffi.olm.OutgoingVerificationRequest import uniffi.olm.ProgressListener as RustProgressListener import uniffi.olm.Request import uniffi.olm.RequestType +import uniffi.olm.Sas as InnerSas +import uniffi.olm.VerificationRequest as InnerRequest import uniffi.olm.setLogger class CryptoLogger : Logger { @@ -89,13 +100,9 @@ fun setRustLogger() { setLogger(CryptoLogger() as Logger) } -/** - * Convert a Rust Device into a Kotlin CryptoDeviceInfo - */ +/** Convert a Rust Device into a Kotlin CryptoDeviceInfo */ private fun toCryptoDeviceInfo(device: Device): CryptoDeviceInfo { - val keys = device.keys.map { (keyId, key) -> - "$keyId:$device.deviceId" to key - }.toMap() + val keys = device.keys.map { (keyId, key) -> "$keyId:$device.deviceId" to key }.toMap() return CryptoDeviceInfo( device.deviceId, @@ -110,8 +117,7 @@ private fun toCryptoDeviceInfo(device: Device): CryptoDeviceInfo { DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), device.isBlocked, // TODO - null - ) + null) } internal class DeviceUpdateObserver { @@ -126,27 +132,171 @@ internal class DeviceUpdateObserver { } } -internal class Sas(private val machine: InnerMachine, private var inner: InnerSas) { +internal class VerificationRequest( + private val machine: InnerMachine, + private var inner: InnerRequest +) { private fun refreshData() { - val sas = this.machine.getVerification(this.inner.flowId) + val request = this.machine.getVerificationRequest(this.inner.otherUserId, this.inner.flowId) - if (sas != null) { - this.inner = sas - } + if (request != null) { + this.inner = request + } - return + return + } + + fun accept_with_methods(methods: List): OutgoingVerificationRequest? { + val stringMethods: MutableList = + methods.map { + when (it) { + VerificationMethod.QR_CODE_SCAN -> VERIFICATION_METHOD_QR_CODE_SCAN + VerificationMethod.QR_CODE_SHOW -> VERIFICATION_METHOD_QR_CODE_SHOW + VerificationMethod.SAS -> VERIFICATION_METHOD_SAS + } + }.toMutableList() + + if (stringMethods.contains(VERIFICATION_METHOD_QR_CODE_SHOW) || + stringMethods.contains(VERIFICATION_METHOD_QR_CODE_SCAN)) { + stringMethods.add(VERIFICATION_METHOD_RECIPROCATE) + } + + return this.machine.acceptVerificationRequest( + this.inner.otherUserId, this.inner.flowId, stringMethods) } fun isCanceled(): Boolean { refreshData() - return this.inner.isCanceled + return this.inner.isCancelled } fun isDone(): Boolean { refreshData() return this.inner.isDone } - + + fun isReady(): Boolean { + refreshData() + return this.inner.isReady + } + + fun toPendingVerificationRequest(): PendingVerificationRequest { + refreshData() + val code = this.inner.cancelCode + + val cancelCode = + if (code != null) { + toCancelCode(code) + } else { + null + } + + val ourMethods = this.inner.ourMethods + val theirMethods = this.inner.theirMethods + val otherDeviceId = this.inner.otherDeviceId + + var requestInfo: ValidVerificationInfoRequest? = null + var readyInfo: ValidVerificationInfoReady? = null + + if (this.inner.weStarted && ourMethods != null) { + requestInfo = + ValidVerificationInfoRequest( + this.inner.flowId, + this.machine.deviceId(), + ourMethods, + null, + ) + } else if (!this.inner.weStarted && ourMethods != null) { + readyInfo = + ValidVerificationInfoReady( + this.inner.flowId, + this.machine.deviceId(), + ourMethods, + ) + } + + if (this.inner.weStarted && theirMethods != null && otherDeviceId != null) { + readyInfo = + ValidVerificationInfoReady( + this.inner.flowId, + otherDeviceId, + theirMethods, + ) + } else if (!this.inner.weStarted && theirMethods != null && otherDeviceId != null) { + requestInfo = + ValidVerificationInfoRequest( + this.inner.flowId, + otherDeviceId, + theirMethods, + System.currentTimeMillis(), + ) + } + + return PendingVerificationRequest( + // Creation time + System.currentTimeMillis(), + // Who initiated the request + !this.inner.weStarted, + // Local echo id, what to do here? + this.inner.flowId, + // other user + this.inner.otherUserId, + // room id + this.inner.roomId, + // transaction id + this.inner.flowId, + // val requestInfo: ValidVerificationInfoRequest? = null, + requestInfo, + // val readyInfo: ValidVerificationInfoReady? = null, + readyInfo, + // cancel code if there is one + cancelCode, + // are we done/successful + this.inner.isDone, + // did another device answer the request + this.inner.isPassive, + // devices that should receive the events we send out + null, + ) + } +} + +private fun toCancelCode(cancelCode: RustCancelCode): CancelCode { + return when (cancelCode) { + RustCancelCode.USER -> CancelCode.User + RustCancelCode.TIMEOUT -> CancelCode.Timeout + RustCancelCode.UNKNOWN_TRANSACTION -> CancelCode.UnknownTransaction + RustCancelCode.UNKNOWN_METHOD -> CancelCode.UnknownMethod + RustCancelCode.UNEXPECTED_MESSAGE -> CancelCode.UnexpectedMessage + RustCancelCode.KEY_MISMATCH -> CancelCode.MismatchedKeys + RustCancelCode.USER_MISMATCH -> CancelCode.MismatchedKeys + RustCancelCode.INVALID_MESSAGE -> CancelCode.InvalidMessage + // TODO why don't the ruma codes match what's in EA? + RustCancelCode.ACCEPTED -> CancelCode.User + } +} + +internal class Sas(private val machine: InnerMachine, private var inner: InnerSas) { + private fun refreshData() { + val sas = this.machine.getVerification(this.inner.flowId) + + if (sas != null) { + this.inner = sas + } + + return + } + + fun isCanceled(): Boolean { + refreshData() + return this.inner.isCancelled + } + + fun isDone(): Boolean { + refreshData() + return this.inner.isDone + } + fun timedOut(): Boolean { refreshData() return this.inner.timedOut @@ -162,9 +312,8 @@ internal class Sas(private val machine: InnerMachine, private var inner: InnerSa } @Throws(CryptoStoreErrorException::class) - suspend fun confirm(): OutgoingVerificationRequest? = withContext(Dispatchers.IO) { - machine.confirmVerification(inner.flowId) - } + suspend fun confirm(): OutgoingVerificationRequest? = + withContext(Dispatchers.IO) { machine.confirmVerification(inner.flowId) } fun cancel(): OutgoingVerificationRequest? { return this.machine.cancelVerification(inner.flowId) @@ -185,27 +334,26 @@ internal class Sas(private val machine: InnerMachine, private var inner: InnerSa } } -internal class OlmMachine(user_id: String, device_id: String, path: File, deviceObserver: DeviceUpdateObserver) { +internal class OlmMachine( + user_id: String, + device_id: String, + path: File, + deviceObserver: DeviceUpdateObserver +) { private val inner: InnerMachine = InnerMachine(user_id, device_id, path.toString()) private val deviceUpdateObserver = deviceObserver - /** - * Get our own user ID. - */ + /** Get our own user ID. */ fun userId(): String { return this.inner.userId() } - /** - * Get our own device ID. - */ + /** Get our own device ID. */ fun deviceId(): String { return this.inner.deviceId() } - /** - * Get our own public identity keys ID. - */ + /** Get our own public identity keys ID. */ fun identityKeys(): Map { return this.inner.identityKeys() } @@ -213,36 +361,31 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device fun ownDevice(): CryptoDeviceInfo { val deviceId = this.deviceId() - val keys = this.identityKeys().map { (keyId, key) -> - "$keyId:$deviceId" to key - }.toMap() + val keys = this.identityKeys().map { (keyId, key) -> "$keyId:$deviceId" to key }.toMap() return CryptoDeviceInfo( - this.deviceId(), - this.userId(), - // TODO pass the algorithms here. - listOf(), - keys, - mapOf(), - UnsignedDeviceInfo(), - DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), - false, - null - ) + this.deviceId(), + this.userId(), + // TODO pass the algorithms here. + listOf(), + keys, + mapOf(), + UnsignedDeviceInfo(), + DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), + false, + null) } /** * Get the list of outgoing requests that need to be sent to the homeserver. * - * After the request was sent out and a successful response was received - * the response body should be passed back to the state machine using the - * markRequestAsSent() method. + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the markRequestAsSent() method. * * @return the list of requests that needs to be sent to the homeserver */ - suspend fun outgoingRequests(): List = withContext(Dispatchers.IO) { - inner.outgoingRequests() - } + suspend fun outgoingRequests(): List = + withContext(Dispatchers.IO) { inner.outgoingRequests() } /** * Mark a request that was sent to the server as sent. @@ -255,135 +398,127 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device */ @Throws(CryptoStoreErrorException::class) suspend fun markRequestAsSent( - requestId: String, - requestType: RequestType, - responseBody: String - ) = withContext(Dispatchers.IO) { - inner.markRequestAsSent(requestId, requestType, responseBody) + requestId: String, + requestType: RequestType, + responseBody: String + ) = + withContext(Dispatchers.IO) { + inner.markRequestAsSent(requestId, requestType, responseBody) - if (requestType == RequestType.KEYS_QUERY) { - updateLiveDevices() - } - } + if (requestType == RequestType.KEYS_QUERY) { + updateLiveDevices() + } + } /** - * Let the state machine know about E2EE related sync changes that we - * received from the server. + * Let the state machine know about E2EE related sync changes that we received from the server. * - * This needs to be called after every sync, ideally before processing - * any other sync changes. + * This needs to be called after every sync, ideally before processing any other sync changes. * - * @param toDevice A serialized array of to-device events we received in the - * current sync resposne. + * @param toDevice A serialized array of to-device events we received in the current sync + * resposne. * - * @param deviceChanges The list of devices that have changed in some way - * since the previous sync. + * @param deviceChanges The list of devices that have changed in some way since the previous + * sync. * * @param keyCounts The map of uploaded one-time key types and counts. */ @Throws(CryptoStoreErrorException::class) suspend fun receiveSyncChanges( - toDevice: ToDeviceSyncResponse?, - deviceChanges: DeviceListResponse?, - keyCounts: DeviceOneTimeKeysCountSyncResponse? - ): ToDeviceSyncResponse = withContext(Dispatchers.IO) { - val counts: MutableMap = mutableMapOf() + toDevice: ToDeviceSyncResponse?, + deviceChanges: DeviceListResponse?, + keyCounts: DeviceOneTimeKeysCountSyncResponse? + ): ToDeviceSyncResponse = + withContext(Dispatchers.IO) { + val counts: MutableMap = mutableMapOf() - if (keyCounts?.signedCurve25519 != null) { - counts["signed_curve25519"] = keyCounts.signedCurve25519 + if (keyCounts?.signedCurve25519 != null) { + counts["signed_curve25519"] = keyCounts.signedCurve25519 + } + + val devices = + DeviceLists( + deviceChanges?.changed ?: listOf(), deviceChanges?.left ?: listOf()) + val adapter = + MoshiProvider.providesMoshi().adapter(ToDeviceSyncResponse::class.java) + val events = adapter.toJson(toDevice ?: ToDeviceSyncResponse())!! + + adapter.fromJson(inner.receiveSyncChanges(events, devices, counts))!! } - val devices = DeviceLists(deviceChanges?.changed ?: listOf(), deviceChanges?.left ?: listOf()) - val adapter = MoshiProvider.providesMoshi().adapter(ToDeviceSyncResponse::class.java) - val events = adapter.toJson(toDevice ?: ToDeviceSyncResponse())!! - - adapter.fromJson(inner.receiveSyncChanges(events, devices, counts))!! - } - /** - * Mark the given list of users to be tracked, triggering a key query request - * for them. + * Mark the given list of users to be tracked, triggering a key query request for them. * - * *Note*: Only users that aren't already tracked will be considered for an - * update. It's safe to call this with already tracked users, it won't - * result in excessive keys query requests. + * *Note*: Only users that aren't already tracked will be considered for an update. It's safe to + * call this with already tracked users, it won't result in excessive keys query requests. * * @param users The users that should be queued up for a key query. */ - suspend fun updateTrackedUsers(users: List) = withContext(Dispatchers.IO) { - inner.updateTrackedUsers(users) - } + suspend fun updateTrackedUsers(users: List) = + withContext(Dispatchers.IO) { inner.updateTrackedUsers(users) } /** - * Generate one-time key claiming requests for all the users we are missing - * sessions for. + * Generate one-time key claiming requests for all the users we are missing sessions for. * - * After the request was sent out and a successful response was received - * the response body should be passed back to the state machine using the - * markRequestAsSent() method. + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the markRequestAsSent() method. * - * This method should be called every time before a call to - * shareRoomKey() is made. + * This method should be called every time before a call to shareRoomKey() is made. * - * @param users The list of users for which we would like to establish 1:1 - * Olm sessions for. + * @param users The list of users for which we would like to establish 1:1 Olm sessions for. * * @return A keys claim request that needs to be sent out to the server. */ @Throws(CryptoStoreErrorException::class) - suspend fun getMissingSessions(users: List): Request? = withContext(Dispatchers.IO) { - inner.getMissingSessions(users) - } + suspend fun getMissingSessions(users: List): Request? = + withContext(Dispatchers.IO) { inner.getMissingSessions(users) } /** * Share a room key with the given list of users for the given room. * - * After the request was sent out and a successful response was received - * the response body should be passed back to the state machine using the - * markRequestAsSent() method. + * After the request was sent out and a successful response was received the response body + * should be passed back to the state machine using the markRequestAsSent() method. * - * This method should be called every time before a call to - * `encrypt()` with the given `room_id` is made. + * This method should be called every time before a call to `encrypt()` with the given `room_id` + * is made. * - * @param roomId The unique id of the room, note that this doesn't strictly - * need to be a Matrix room, it just needs to be an unique identifier for - * the group that will participate in the conversation. + * @param roomId The unique id of the room, note that this doesn't strictly need to be a Matrix + * room, it just needs to be an unique identifier for the group that will participate in the + * conversation. * - * @param users The list of users which are considered to be members of the - * room and should receive the room key. + * @param users The list of users which are considered to be members of the room and should + * receive the room key. * * @return The list of requests that need to be sent out. */ @Throws(CryptoStoreErrorException::class) - suspend fun shareRoomKey(roomId: String, users: List): List = withContext(Dispatchers.IO) { - inner.shareRoomKey(roomId, users) - } + suspend fun shareRoomKey(roomId: String, users: List): List = + withContext(Dispatchers.IO) { inner.shareRoomKey(roomId, users) } /** - * Encrypt the given event with the given type and content for the given - * room. + * Encrypt the given event with the given type and content for the given room. * - * **Note**: A room key needs to be shared with the group of users that are - * members in the given room. If this is not done this method will panic. + * **Note**: A room key needs to be shared with the group of users that are members in the given + * room. If this is not done this method will panic. * - * The usual flow to encrypt an evnet using this state machine is as - * follows: + * The usual flow to encrypt an evnet using this state machine is as follows: * * 1. Get the one-time key claim request to establish 1:1 Olm sessions for + * ``` * the room members of the room we wish to participate in. This is done * using the [`get_missing_sessions()`](#method.get_missing_sessions) * method. This method call should be locked per call. - * + * ``` * 2. Share a room key with all the room members using the shareRoomKey(). + * ``` * This method call should be locked per room. - * + * ``` * 3. Encrypt the event using this method. * * 4. Send the encrypted event to the server. * - * After the room key is shared steps 1 and 2 will become noops, unless - * there's some changes in the room membership or in the list of devices a - * member has. + * After the room key is shared steps 1 and 2 will become noops, unless there's some changes in + * the room membership or in the list of devices a member has. * * @param roomId the ID of the room where the encrypted event will be sent to * @@ -394,12 +529,13 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device * @return The encrypted version of the content */ @Throws(CryptoStoreErrorException::class) - suspend fun encrypt(roomId: String, eventType: String, content: Content): Content = withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) - val contentString = adapter.toJson(content) - val encrypted = inner.encrypt(roomId, eventType, contentString) - adapter.fromJson(encrypted)!! - } + suspend fun encrypt(roomId: String, eventType: String, content: Content): Content = + withContext(Dispatchers.IO) { + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + val contentString = adapter.toJson(content) + val encrypted = inner.encrypt(roomId, eventType, contentString) + adapter.fromJson(encrypted)!! + } /** * Decrypt the given event that was sent in the given room. @@ -411,62 +547,64 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device * @return the decrypted version of the event. */ @Throws(MXCryptoError::class) - suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult = withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) - val serializedEvent = adapter.toJson(event) + suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult = + withContext(Dispatchers.IO) { + val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) + val serializedEvent = adapter.toJson(event) - try { - val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId!!) + try { + val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId!!) - val deserializationAdapter = MoshiProvider.providesMoshi().adapter(Map::class.java) - val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent)!! + val deserializationAdapter = + MoshiProvider.providesMoshi().adapter(Map::class.java) + val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent)!! - MXEventDecryptionResult( - clearEvent, - decrypted.senderCurve25519Key, - decrypted.claimedEd25519Key, - decrypted.forwardingCurve25519Chain - ) - } catch (throwable: Throwable) { - val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, throwable.message, "m.megolm.v1.aes-sha2") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) - } - } + MXEventDecryptionResult( + clearEvent, + decrypted.senderCurve25519Key, + decrypted.claimedEd25519Key, + decrypted.forwardingCurve25519Chain) + } catch (throwable: Throwable) { + val reason = + String.format( + MXCryptoError.UNABLE_TO_DECRYPT_REASON, + throwable.message, + "m.megolm.v1.aes-sha2") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } + } /** - * Request the room key that was used to encrypt the given undecrypted - * event. + * Request the room key that was used to encrypt the given undecrypted event. * - * @param event The that we're not able to decrypt and want to request a - * room key for. + * @param event The that we're not able to decrypt and want to request a room key for. * - * @return a key request pair, consisting of an optional key request - * cancellation and the key request itself. The cancellation *must* be sent - * out before the request, otherwise devices will ignore the key request. + * @return a key request pair, consisting of an optional key request cancellation and the key + * request itself. The cancellation *must* be sent out before the request, otherwise devices + * will ignore the key request. */ @Throws(DecryptionErrorException::class) - suspend fun requestRoomKey(event: Event): KeyRequestPair = withContext(Dispatchers.IO) { - val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) - val serializedEvent = adapter.toJson(event) + suspend fun requestRoomKey(event: Event): KeyRequestPair = + withContext(Dispatchers.IO) { + val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) + val serializedEvent = adapter.toJson(event) - inner.requestRoomKey(serializedEvent, event.roomId!!) - } + inner.requestRoomKey(serializedEvent, event.roomId!!) + } /** * Export all of our room keys. * - * @param passphrase The passphrase that should be used to encrypt the key - * export. + * @param passphrase The passphrase that should be used to encrypt the key export. * - * @param rounds The number of rounds that should be used when expanding the - * passphrase into an key. + * @param rounds The number of rounds that should be used when expanding the passphrase into an + * key. * * @return the encrypted key export as a bytearray. */ @Throws(CryptoStoreErrorException::class) - suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray = withContext(Dispatchers.IO) { - inner.exportKeys(passphrase, rounds).toByteArray() - } + suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray = + withContext(Dispatchers.IO) { inner.exportKeys(passphrase, rounds).toByteArray() } /** * Import room keys from the given serialized key export. @@ -475,19 +613,23 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device * * @param passphrase The passphrase that was used to encrypt the key export. * - * @param listener A callback that can be used to introspect the - * progress of the key import. + * @param listener A callback that can be used to introspect the progress of the key import. */ @Throws(CryptoStoreErrorException::class) - suspend fun importKeys(keys: ByteArray, passphrase: String, listener: ProgressListener?): ImportRoomKeysResult = withContext(Dispatchers.IO) { - val decodedKeys = String(keys, Charset.defaultCharset()) + suspend fun importKeys( + keys: ByteArray, + passphrase: String, + listener: ProgressListener? + ): ImportRoomKeysResult = + withContext(Dispatchers.IO) { + val decodedKeys = String(keys, Charset.defaultCharset()) - val rustListener = CryptoProgressListener(listener) + val rustListener = CryptoProgressListener(listener) - val result = inner.importKeys(decodedKeys, passphrase, rustListener) + val result = inner.importKeys(decodedKeys, passphrase, rustListener) - ImportRoomKeysResult(result.total, result.imported) - } + ImportRoomKeysResult(result.total, result.imported) + } /** * Get a `Device` from the store. @@ -499,16 +641,17 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device * @return The Device if it found one. */ @Throws(CryptoStoreErrorException::class) - suspend fun getDevice(userId: String, deviceId: String): CryptoDeviceInfo? = withContext(Dispatchers.IO) { - // Our own device isn't part of our store on the rust side, return it - // using our ownDevice method - if (userId == userId() && deviceId == deviceId()) { - ownDevice() - } else { - val device = inner.getDevice(userId, deviceId) - if (device != null) toCryptoDeviceInfo(device) else null - } - } + suspend fun getDevice(userId: String, deviceId: String): CryptoDeviceInfo? = + withContext(Dispatchers.IO) { + // Our own device isn't part of our store on the rust side, return it + // using our ownDevice method + if (userId == userId() && deviceId == deviceId()) { + ownDevice() + } else { + val device = inner.getDevice(userId, deviceId) + if (device != null) toCryptoDeviceInfo(device) else null + } + } /** * Get all devices of an user. @@ -561,9 +704,7 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device return plainDevices } - /** - * Update all of our live device listeners. - */ + /** Update all of our live device listeners. */ private suspend fun updateLiveDevices() { for ((liveDevice, users) in deviceUpdateObserver.listeners) { val devices = getUserDevices(users) @@ -574,8 +715,8 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device /** * Get all the devices of multiple users as a live version. * - * The live version will update the list of devices if some of the data - * changes, or if new devices arrive for a certain user. + * The live version will update the list of devices if some of the data changes, or if new + * devices arrive for a certain user. * * @param userIds The ids of the device owners. * @@ -589,24 +730,36 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device return devices } - /** - * Discard the currently active room key for the given room if there is one. - */ + /** Discard the currently active room key for the given room if there is one. */ @Throws(CryptoStoreErrorException::class) fun discardRoomKey(roomId: String) { runBlocking { inner.discardRoomKey(roomId) } } - /** - * Get an active verification - */ - fun getVerification(flowId: String): Sas? { - val sas = this.inner.getVerification(flowId) + fun getVerificationRequests(userId: String): List { + return this.inner.getVerificationRequests(userId).map { + VerificationRequest(this.inner, it) + } + } - return if (sas == null) { - null - } else { - Sas(this.inner, sas) - } - } + fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? { + val request = this.inner.getVerificationRequest(userId, flowId) + + return if (request == null) { + null + } else { + VerificationRequest(this.inner, request) + } + } + + /** Get an active verification */ + fun getVerification(flowId: String): Sas? { + val sas = this.inner.getVerification(flowId) + + return if (sas == null) { + null + } else { + Sas(this.inner, sas) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt new file mode 100644 index 0000000000..2acad69ebf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/RustVerificationService.kt @@ -0,0 +1,326 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.verification + +import android.os.Handler +import android.os.Looper +import javax.inject.Inject +import kotlin.collections.set +import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationRequest +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.TaskExecutor +import timber.log.Timber +import uniffi.olm.OutgoingVerificationRequest + +@SessionScope +internal class RustVerificationService +@Inject +constructor( + private val taskExecutor: TaskExecutor, + private val olmMachine: OlmMachine, + private val sendToDeviceTask: SendToDeviceTask, +) : DefaultVerificationTransaction.Listener, VerificationService { + + private val uiHandler = Handler(Looper.getMainLooper()) + private var listeners = ArrayList() + + override fun addListener(listener: VerificationService.Listener) { + uiHandler.post { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + } + + override fun removeListener(listener: VerificationService.Listener) { + uiHandler.post { listeners.remove(listener) } + } + + private fun dispatchTxAdded(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchTxUpdated(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId} ${tx}") + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + TODO() + // setDeviceVerificationAction.handle(DeviceTrustLevel(false, true), + // userId, + // deviceID) + + // listeners.forEach { + // try { + // it.markedAsManuallyVerified(userId, deviceID) + // } catch (e: Throwable) { + // Timber.e(e, "## Error while notifying listeners") + // } + // } + } + + suspend fun onEvent(event: Event) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> {} + EventType.KEY_VERIFICATION_CANCEL -> {} + EventType.KEY_VERIFICATION_ACCEPT -> {} + EventType.KEY_VERIFICATION_KEY -> {} + EventType.KEY_VERIFICATION_MAC -> {} + EventType.KEY_VERIFICATION_READY -> {} + EventType.KEY_VERIFICATION_DONE -> {} + MessageType.MSGTYPE_VERIFICATION_REQUEST -> { + onRequestReceived(event) + } + else -> { + // ignore + } + } + event == event + // TODO get the sender and flow id out of the event and depending on the + // event type either get the verification request or verification and + // dispatch updates here + } + + private fun onRequestReceived(event: Event) { + val content = event.getClearContent().toModel() ?: return + val flowId = content.transactionId + val sender = event.senderId ?: return + + val request = this.getExistingVerificationRequest(sender, flowId) ?: return + + dispatchRequestAdded(request) + } + + override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // TODO This should be handled inside the rust-sdk decryption method + } + + // TODO All this methods should be delegated to a TransactionStore + override fun getExistingTransaction( + otherUserId: String, + tid: String + ): VerificationTransaction? { + return null + } + + override fun getExistingVerificationRequests( + otherUserId: String + ): List { + return this.olmMachine.getVerificationRequests(otherUserId).map { + it.toPendingVerificationRequest() + } + } + + override fun getExistingVerificationRequest( + otherUserId: String, + tid: String? + ): PendingVerificationRequest? { + return if (tid != null) { + val request = this.olmMachine.getVerificationRequest(otherUserId, tid) + + if (request != null) { + request.toPendingVerificationRequest() + } else { + null + } + } else { + null + } + } + + override fun getExistingVerificationRequestInRoom( + roomId: String, + tid: String? + ): PendingVerificationRequest? { + TODO() + } + + override fun beginKeyVerification( + method: VerificationMethod, + otherUserId: String, + otherDeviceId: String, + transactionId: String? + ): String? { + // should check if already one (and cancel it) + if (method == VerificationMethod.SAS) { + // TODO start SAS verification here, don't we need to see if there's + // a request? + TODO() + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + override fun requestKeyVerificationInDMs( + methods: List, + otherUserId: String, + roomId: String, + localId: String? + ): PendingVerificationRequest { + Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") + + // TODO cancel other active requests, create a new request here and + // dispatch it + TODO() + } + + override fun requestKeyVerification( + methods: List, + otherUserId: String, + otherDevices: List? + ): PendingVerificationRequest { + // This was mostly a copy paste of the InDMs method, do the same here + TODO() + } + + override fun cancelVerificationRequest(request: PendingVerificationRequest) { + // TODO get the request out of the olm machine and cancel here + TODO() + } + + override fun declineVerificationRequestInDMs( + otherUserId: String, + transactionId: String, + roomId: String + ) { + // TODO get an existing verification request out of the olm machine and + // cancel it. update the pending request afterwards + } + + override fun beginKeyVerificationInDMs( + method: VerificationMethod, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String + ): String { + // TODO fetch out the verification request nad start SAS, return the + // flow id + return "" + } + + override fun readyPendingVerificationInDMs( + methods: List, + otherUserId: String, + roomId: String, + transactionId: String + ): Boolean { + Timber.e("## TRYING TO READY PENDING ROOM VERIFICATION") + // TODO do the same as readyPendingVerification + return true + } + + override fun readyPendingVerification( + methods: List, + otherUserId: String, + transactionId: String + ): Boolean { + val request = this.olmMachine.getVerificationRequest(otherUserId, transactionId) + + return if (request != null) { + val outgoingRequest = request.accept_with_methods(methods) + + if (outgoingRequest != null) { + runBlocking { sendRequest(outgoingRequest) } + dispatchRequestUpdated(request.toPendingVerificationRequest()) + true + } else { + false + } + } else { + false + } + } + + suspend fun sendRequest(request: OutgoingVerificationRequest) { + when (request) { + is OutgoingVerificationRequest.ToDevice -> { + val adapter = + MoshiProvider.providesMoshi() + .adapter>>(Map::class.java) + val body = adapter.fromJson(request.body)!! + + val userMap = MXUsersDevicesMap() + userMap.join(body) + + val sendToDeviceParams = SendToDeviceTask.Params(request.eventType, userMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + else -> {} + } + + // TODO move this into the VerificationRequest and Verification classes? + } + + override fun transactionUpdated(tx: VerificationTransaction) { + dispatchTxUpdated(tx) + } +}