crypto: Initial support to answer to-device verification requests

This commit is contained in:
Damir Jelić 2021-06-17 13:38:30 +02:00
parent e46578a087
commit a4e1a5bbcb
3 changed files with 724 additions and 213 deletions

View File

@ -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.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.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.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask 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.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.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.SessionFilesDirectory
@ -131,9 +132,6 @@ internal class DefaultCryptoService @Inject constructor(
private val mxCryptoConfig: MXCryptoConfig, private val mxCryptoConfig: MXCryptoConfig,
// The key backup service. // The key backup service.
private val keysBackupService: DefaultKeysBackupService, private val keysBackupService: DefaultKeysBackupService,
// The verification service.
private val verificationService: DefaultVerificationService,
private val crossSigningService: DefaultCrossSigningService, private val crossSigningService: DefaultCrossSigningService,
// Actions // Actions
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
@ -156,6 +154,9 @@ internal class DefaultCryptoService @Inject constructor(
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private var olmMachine: OlmMachine? = null private var olmMachine: OlmMachine? = null
// The verification service.
private var verificationService: RustVerificationService? = null
private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver() private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver()
// Locks for some of our operations // 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_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(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 { try {
setRustLogger() 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( Timber.v(
"## CRYPTO | Successfully started up an Olm machine for " + "## CRYPTO | Successfully started up an Olm machine for " +
"${userId}, ${deviceId}, identity keys: ${this.olmMachine?.identityKeys()}") "${userId}, ${deviceId}, identity keys: ${this.olmMachine?.identityKeys()}")
@ -359,7 +364,32 @@ internal class DefaultCryptoService @Inject constructor(
/** /**
* @return the VerificationService * @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 override fun crossSigningService() = crossSigningService
@ -677,6 +707,8 @@ internal class DefaultCryptoService @Inject constructor(
val sessionId = content.sessionId val sessionId = content.sessionId
notifyRoomKeyReceival(roomId, sessionId) notifyRoomKeyReceival(roomId, sessionId)
} else {
this.verificationService?.onEvent(event)
} }
} }
} }

View File

@ -26,33 +26,44 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.listeners.ProgressListener 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.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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel 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.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult 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.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo 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.crypto.verification.getEmojiForCode
import org.matrix.android.sdk.internal.di.MoshiProvider 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.DeviceListResponse
import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse
import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse
import timber.log.Timber import timber.log.Timber
import uniffi.olm.CancelCode as RustCancelCode
import uniffi.olm.CryptoStoreErrorException import uniffi.olm.CryptoStoreErrorException
import uniffi.olm.DecryptionErrorException import uniffi.olm.DecryptionErrorException
import uniffi.olm.Sas as InnerSas
import uniffi.olm.OutgoingVerificationRequest
import uniffi.olm.Device import uniffi.olm.Device
import uniffi.olm.DeviceLists import uniffi.olm.DeviceLists
import uniffi.olm.KeyRequestPair import uniffi.olm.KeyRequestPair
import uniffi.olm.Logger import uniffi.olm.Logger
import uniffi.olm.OlmMachine as InnerMachine import uniffi.olm.OlmMachine as InnerMachine
import uniffi.olm.OutgoingVerificationRequest
import uniffi.olm.ProgressListener as RustProgressListener import uniffi.olm.ProgressListener as RustProgressListener
import uniffi.olm.Request import uniffi.olm.Request
import uniffi.olm.RequestType import uniffi.olm.RequestType
import uniffi.olm.Sas as InnerSas
import uniffi.olm.VerificationRequest as InnerRequest
import uniffi.olm.setLogger import uniffi.olm.setLogger
class CryptoLogger : Logger { class CryptoLogger : Logger {
@ -89,13 +100,9 @@ fun setRustLogger() {
setLogger(CryptoLogger() as Logger) 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 { private fun toCryptoDeviceInfo(device: Device): CryptoDeviceInfo {
val keys = device.keys.map { (keyId, key) -> val keys = device.keys.map { (keyId, key) -> "$keyId:$device.deviceId" to key }.toMap()
"$keyId:$device.deviceId" to key
}.toMap()
return CryptoDeviceInfo( return CryptoDeviceInfo(
device.deviceId, device.deviceId,
@ -110,8 +117,7 @@ private fun toCryptoDeviceInfo(device: Device): CryptoDeviceInfo {
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false), DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false),
device.isBlocked, device.isBlocked,
// TODO // TODO
null null)
)
} }
internal class DeviceUpdateObserver { 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() { 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) { if (request != null) {
this.inner = sas this.inner = request
} }
return return
}
fun accept_with_methods(methods: List<VerificationMethod>): OutgoingVerificationRequest? {
val stringMethods: MutableList<String> =
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 { fun isCanceled(): Boolean {
refreshData() refreshData()
return this.inner.isCanceled return this.inner.isCancelled
} }
fun isDone(): Boolean { fun isDone(): Boolean {
refreshData() refreshData()
return this.inner.isDone 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 { fun timedOut(): Boolean {
refreshData() refreshData()
return this.inner.timedOut return this.inner.timedOut
@ -162,9 +312,8 @@ internal class Sas(private val machine: InnerMachine, private var inner: InnerSa
} }
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun confirm(): OutgoingVerificationRequest? = withContext(Dispatchers.IO) { suspend fun confirm(): OutgoingVerificationRequest? =
machine.confirmVerification(inner.flowId) withContext(Dispatchers.IO) { machine.confirmVerification(inner.flowId) }
}
fun cancel(): OutgoingVerificationRequest? { fun cancel(): OutgoingVerificationRequest? {
return this.machine.cancelVerification(inner.flowId) 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 inner: InnerMachine = InnerMachine(user_id, device_id, path.toString())
private val deviceUpdateObserver = deviceObserver private val deviceUpdateObserver = deviceObserver
/** /** Get our own user ID. */
* Get our own user ID.
*/
fun userId(): String { fun userId(): String {
return this.inner.userId() return this.inner.userId()
} }
/** /** Get our own device ID. */
* Get our own device ID.
*/
fun deviceId(): String { fun deviceId(): String {
return this.inner.deviceId() return this.inner.deviceId()
} }
/** /** Get our own public identity keys ID. */
* Get our own public identity keys ID.
*/
fun identityKeys(): Map<String, String> { fun identityKeys(): Map<String, String> {
return this.inner.identityKeys() return this.inner.identityKeys()
} }
@ -213,36 +361,31 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device
fun ownDevice(): CryptoDeviceInfo { fun ownDevice(): CryptoDeviceInfo {
val deviceId = this.deviceId() val deviceId = this.deviceId()
val keys = this.identityKeys().map { (keyId, key) -> val keys = this.identityKeys().map { (keyId, key) -> "$keyId:$deviceId" to key }.toMap()
"$keyId:$deviceId" to key
}.toMap()
return CryptoDeviceInfo( return CryptoDeviceInfo(
this.deviceId(), this.deviceId(),
this.userId(), this.userId(),
// TODO pass the algorithms here. // TODO pass the algorithms here.
listOf(), listOf(),
keys, keys,
mapOf(), mapOf(),
UnsignedDeviceInfo(), UnsignedDeviceInfo(),
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
false, false,
null null)
)
} }
/** /**
* Get the list of outgoing requests that need to be sent to the homeserver. * 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 * After the request was sent out and a successful response was received the response body
* the response body should be passed back to the state machine using the * should be passed back to the state machine using the markRequestAsSent() method.
* markRequestAsSent() method.
* *
* @return the list of requests that needs to be sent to the homeserver * @return the list of requests that needs to be sent to the homeserver
*/ */
suspend fun outgoingRequests(): List<Request> = withContext(Dispatchers.IO) { suspend fun outgoingRequests(): List<Request> =
inner.outgoingRequests() withContext(Dispatchers.IO) { inner.outgoingRequests() }
}
/** /**
* Mark a request that was sent to the server as sent. * 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) @Throws(CryptoStoreErrorException::class)
suspend fun markRequestAsSent( suspend fun markRequestAsSent(
requestId: String, requestId: String,
requestType: RequestType, requestType: RequestType,
responseBody: String responseBody: String
) = withContext(Dispatchers.IO) { ) =
inner.markRequestAsSent(requestId, requestType, responseBody) withContext(Dispatchers.IO) {
inner.markRequestAsSent(requestId, requestType, responseBody)
if (requestType == RequestType.KEYS_QUERY) { if (requestType == RequestType.KEYS_QUERY) {
updateLiveDevices() updateLiveDevices()
} }
} }
/** /**
* Let the state machine know about E2EE related sync changes that we * Let the state machine know about E2EE related sync changes that we received from the server.
* received from the server.
* *
* This needs to be called after every sync, ideally before processing * This needs to be called after every sync, ideally before processing any other sync changes.
* any other sync changes.
* *
* @param toDevice A serialized array of to-device events we received in the * @param toDevice A serialized array of to-device events we received in the current sync
* current sync resposne. * resposne.
* *
* @param deviceChanges The list of devices that have changed in some way * @param deviceChanges The list of devices that have changed in some way since the previous
* since the previous sync. * sync.
* *
* @param keyCounts The map of uploaded one-time key types and counts. * @param keyCounts The map of uploaded one-time key types and counts.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun receiveSyncChanges( suspend fun receiveSyncChanges(
toDevice: ToDeviceSyncResponse?, toDevice: ToDeviceSyncResponse?,
deviceChanges: DeviceListResponse?, deviceChanges: DeviceListResponse?,
keyCounts: DeviceOneTimeKeysCountSyncResponse? keyCounts: DeviceOneTimeKeysCountSyncResponse?
): ToDeviceSyncResponse = withContext(Dispatchers.IO) { ): ToDeviceSyncResponse =
val counts: MutableMap<String, Int> = mutableMapOf() withContext(Dispatchers.IO) {
val counts: MutableMap<String, Int> = mutableMapOf()
if (keyCounts?.signedCurve25519 != null) { if (keyCounts?.signedCurve25519 != null) {
counts["signed_curve25519"] = keyCounts.signedCurve25519 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 * Mark the given list of users to be tracked, triggering a key query request for them.
* for them.
* *
* *Note*: Only users that aren't already tracked will be considered for an * *Note*: Only users that aren't already tracked will be considered for an update. It's safe to
* update. It's safe to call this with already tracked users, it won't * call this with already tracked users, it won't result in excessive keys query requests.
* result in excessive keys query requests.
* *
* @param users The users that should be queued up for a key query. * @param users The users that should be queued up for a key query.
*/ */
suspend fun updateTrackedUsers(users: List<String>) = withContext(Dispatchers.IO) { suspend fun updateTrackedUsers(users: List<String>) =
inner.updateTrackedUsers(users) withContext(Dispatchers.IO) { inner.updateTrackedUsers(users) }
}
/** /**
* Generate one-time key claiming requests for all the users we are missing * Generate one-time key claiming requests for all the users we are missing sessions for.
* sessions for.
* *
* After the request was sent out and a successful response was received * After the request was sent out and a successful response was received the response body
* the response body should be passed back to the state machine using the * should be passed back to the state machine using the markRequestAsSent() method.
* markRequestAsSent() method.
* *
* This method should be called every time before a call to * This method should be called every time before a call to shareRoomKey() is made.
* shareRoomKey() is made.
* *
* @param users The list of users for which we would like to establish 1:1 * @param users The list of users for which we would like to establish 1:1 Olm sessions for.
* Olm sessions for.
* *
* @return A keys claim request that needs to be sent out to the server. * @return A keys claim request that needs to be sent out to the server.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun getMissingSessions(users: List<String>): Request? = withContext(Dispatchers.IO) { suspend fun getMissingSessions(users: List<String>): Request? =
inner.getMissingSessions(users) withContext(Dispatchers.IO) { inner.getMissingSessions(users) }
}
/** /**
* Share a room key with the given list of users for the given room. * 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 * After the request was sent out and a successful response was received the response body
* the response body should be passed back to the state machine using the * should be passed back to the state machine using the markRequestAsSent() method.
* markRequestAsSent() method.
* *
* This method should be called every time before a call to * This method should be called every time before a call to `encrypt()` with the given `room_id`
* `encrypt()` with the given `room_id` is made. * is made.
* *
* @param roomId The unique id of the room, note that this doesn't strictly * @param roomId The unique id of the room, note that this doesn't strictly need to be a Matrix
* need to be a Matrix room, it just needs to be an unique identifier for * room, it just needs to be an unique identifier for the group that will participate in the
* the group that will participate in the conversation. * conversation.
* *
* @param users The list of users which are considered to be members of the * @param users The list of users which are considered to be members of the room and should
* room and should receive the room key. * receive the room key.
* *
* @return The list of requests that need to be sent out. * @return The list of requests that need to be sent out.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun shareRoomKey(roomId: String, users: List<String>): List<Request> = withContext(Dispatchers.IO) { suspend fun shareRoomKey(roomId: String, users: List<String>): List<Request> =
inner.shareRoomKey(roomId, users) withContext(Dispatchers.IO) { inner.shareRoomKey(roomId, users) }
}
/** /**
* Encrypt the given event with the given type and content for the given * Encrypt the given event with the given type and content for the given room.
* room.
* *
* **Note**: A room key needs to be shared with the group of users that are * **Note**: A room key needs to be shared with the group of users that are members in the given
* members in the given room. If this is not done this method will panic. * room. If this is not done this method will panic.
* *
* The usual flow to encrypt an evnet using this state machine is as * The usual flow to encrypt an evnet using this state machine is as follows:
* follows:
* *
* 1. Get the one-time key claim request to establish 1:1 Olm sessions for * 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 * the room members of the room we wish to participate in. This is done
* using the [`get_missing_sessions()`](#method.get_missing_sessions) * using the [`get_missing_sessions()`](#method.get_missing_sessions)
* method. This method call should be locked per call. * method. This method call should be locked per call.
* * ```
* 2. Share a room key with all the room members using the shareRoomKey(). * 2. Share a room key with all the room members using the shareRoomKey().
* ```
* This method call should be locked per room. * This method call should be locked per room.
* * ```
* 3. Encrypt the event using this method. * 3. Encrypt the event using this method.
* *
* 4. Send the encrypted event to the server. * 4. Send the encrypted event to the server.
* *
* After the room key is shared steps 1 and 2 will become noops, unless * After the room key is shared steps 1 and 2 will become noops, unless there's some changes in
* there's some changes in the room membership or in the list of devices a * the room membership or in the list of devices a member has.
* member has.
* *
* @param roomId the ID of the room where the encrypted event will be sent to * @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 * @return The encrypted version of the content
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun encrypt(roomId: String, eventType: String, content: Content): Content = withContext(Dispatchers.IO) { suspend fun encrypt(roomId: String, eventType: String, content: Content): Content =
val adapter = MoshiProvider.providesMoshi().adapter<Content>(Map::class.java) withContext(Dispatchers.IO) {
val contentString = adapter.toJson(content) val adapter = MoshiProvider.providesMoshi().adapter<Content>(Map::class.java)
val encrypted = inner.encrypt(roomId, eventType, contentString) val contentString = adapter.toJson(content)
adapter.fromJson(encrypted)!! val encrypted = inner.encrypt(roomId, eventType, contentString)
} adapter.fromJson(encrypted)!!
}
/** /**
* Decrypt the given event that was sent in the given room. * 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. * @return the decrypted version of the event.
*/ */
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult = withContext(Dispatchers.IO) { suspend fun decryptRoomEvent(event: Event): MXEventDecryptionResult =
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) withContext(Dispatchers.IO) {
val serializedEvent = adapter.toJson(event) val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
val serializedEvent = adapter.toJson(event)
try { try {
val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId!!) val decrypted = inner.decryptRoomEvent(serializedEvent, event.roomId!!)
val deserializationAdapter = MoshiProvider.providesMoshi().adapter<JsonDict>(Map::class.java) val deserializationAdapter =
val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent)!! MoshiProvider.providesMoshi().adapter<JsonDict>(Map::class.java)
val clearEvent = deserializationAdapter.fromJson(decrypted.clearEvent)!!
MXEventDecryptionResult( MXEventDecryptionResult(
clearEvent, clearEvent,
decrypted.senderCurve25519Key, decrypted.senderCurve25519Key,
decrypted.claimedEd25519Key, decrypted.claimedEd25519Key,
decrypted.forwardingCurve25519Chain decrypted.forwardingCurve25519Chain)
) } catch (throwable: Throwable) {
} catch (throwable: Throwable) { val reason =
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, throwable.message, "m.megolm.v1.aes-sha2") String.format(
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) 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 * Request the room key that was used to encrypt the given undecrypted event.
* event.
* *
* @param event The that we're not able to decrypt and want to request a * @param event The that we're not able to decrypt and want to request a room key for.
* room key for.
* *
* @return a key request pair, consisting of an optional key request * @return a key request pair, consisting of an optional key request cancellation and the key
* cancellation and the key request itself. The cancellation *must* be sent * request itself. The cancellation *must* be sent out before the request, otherwise devices
* out before the request, otherwise devices will ignore the key request. * will ignore the key request.
*/ */
@Throws(DecryptionErrorException::class) @Throws(DecryptionErrorException::class)
suspend fun requestRoomKey(event: Event): KeyRequestPair = withContext(Dispatchers.IO) { suspend fun requestRoomKey(event: Event): KeyRequestPair =
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java) withContext(Dispatchers.IO) {
val serializedEvent = adapter.toJson(event) 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. * Export all of our room keys.
* *
* @param passphrase The passphrase that should be used to encrypt the key * @param passphrase The passphrase that should be used to encrypt the key export.
* export.
* *
* @param rounds The number of rounds that should be used when expanding the * @param rounds The number of rounds that should be used when expanding the passphrase into an
* passphrase into an key. * key.
* *
* @return the encrypted key export as a bytearray. * @return the encrypted key export as a bytearray.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray = withContext(Dispatchers.IO) { suspend fun exportKeys(passphrase: String, rounds: Int): ByteArray =
inner.exportKeys(passphrase, rounds).toByteArray() withContext(Dispatchers.IO) { inner.exportKeys(passphrase, rounds).toByteArray() }
}
/** /**
* Import room keys from the given serialized key export. * 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 passphrase The passphrase that was used to encrypt the key export.
* *
* @param listener A callback that can be used to introspect the * @param listener A callback that can be used to introspect the progress of the key import.
* progress of the key import.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun importKeys(keys: ByteArray, passphrase: String, listener: ProgressListener?): ImportRoomKeysResult = withContext(Dispatchers.IO) { suspend fun importKeys(
val decodedKeys = String(keys, Charset.defaultCharset()) 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. * 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. * @return The Device if it found one.
*/ */
@Throws(CryptoStoreErrorException::class) @Throws(CryptoStoreErrorException::class)
suspend fun getDevice(userId: String, deviceId: String): CryptoDeviceInfo? = withContext(Dispatchers.IO) { suspend fun getDevice(userId: String, deviceId: String): CryptoDeviceInfo? =
// Our own device isn't part of our store on the rust side, return it withContext(Dispatchers.IO) {
// using our ownDevice method // Our own device isn't part of our store on the rust side, return it
if (userId == userId() && deviceId == deviceId()) { // using our ownDevice method
ownDevice() if (userId == userId() && deviceId == deviceId()) {
} else { ownDevice()
val device = inner.getDevice(userId, deviceId) } else {
if (device != null) toCryptoDeviceInfo(device) else null val device = inner.getDevice(userId, deviceId)
} if (device != null) toCryptoDeviceInfo(device) else null
} }
}
/** /**
* Get all devices of an user. * Get all devices of an user.
@ -561,9 +704,7 @@ internal class OlmMachine(user_id: String, device_id: String, path: File, device
return plainDevices return plainDevices
} }
/** /** Update all of our live device listeners. */
* Update all of our live device listeners.
*/
private suspend fun updateLiveDevices() { private suspend fun updateLiveDevices() {
for ((liveDevice, users) in deviceUpdateObserver.listeners) { for ((liveDevice, users) in deviceUpdateObserver.listeners) {
val devices = getUserDevices(users) 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. * 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 * The live version will update the list of devices if some of the data changes, or if new
* changes, or if new devices arrive for a certain user. * devices arrive for a certain user.
* *
* @param userIds The ids of the device owners. * @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 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) @Throws(CryptoStoreErrorException::class)
fun discardRoomKey(roomId: String) { fun discardRoomKey(roomId: String) {
runBlocking { inner.discardRoomKey(roomId) } runBlocking { inner.discardRoomKey(roomId) }
} }
/** fun getVerificationRequests(userId: String): List<VerificationRequest> {
* Get an active verification return this.inner.getVerificationRequests(userId).map {
*/ VerificationRequest(this.inner, it)
fun getVerification(flowId: String): Sas? { }
val sas = this.inner.getVerification(flowId) }
return if (sas == null) { fun getVerificationRequest(userId: String, flowId: String): VerificationRequest? {
null val request = this.inner.getVerificationRequest(userId, flowId)
} else {
Sas(this.inner, sas) 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)
}
}
} }

View File

@ -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<VerificationService.Listener>()
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<KeyVerificationRequest>() ?: 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<PendingVerificationRequest> {
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<VerificationMethod>,
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<VerificationMethod>,
otherUserId: String,
otherDevices: List<String>?
): 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<VerificationMethod>,
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<VerificationMethod>,
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<String, HashMap<String, Any>>>(Map::class.java)
val body = adapter.fromJson(request.body)!!
val userMap = MXUsersDevicesMap<Any>()
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)
}
}