All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.net.folivo.trixnity.client.verification.VerificationService.kt Maven / Gradle / Ivy

There is a newer version: 4.7.1
Show newest version
package net.folivo.trixnity.client.verification

import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import net.folivo.trixnity.client.CurrentSyncState
import net.folivo.trixnity.client.key.KeySecretService
import net.folivo.trixnity.client.key.KeyService
import net.folivo.trixnity.client.key.KeyTrustService
import net.folivo.trixnity.client.room.RoomService
import net.folivo.trixnity.client.store.*
import net.folivo.trixnity.client.verification.ActiveVerificationState.Cancel
import net.folivo.trixnity.client.verification.ActiveVerificationState.Done
import net.folivo.trixnity.client.verification.VerificationService.SelfVerificationMethods
import net.folivo.trixnity.client.verification.VerificationService.SelfVerificationMethods.PreconditionsNotMet
import net.folivo.trixnity.clientserverapi.client.MatrixClientServerApiClient
import net.folivo.trixnity.clientserverapi.client.SyncState
import net.folivo.trixnity.core.EventHandler
import net.folivo.trixnity.core.UserInfo
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.ClientEvent
import net.folivo.trixnity.core.model.events.ClientEvent.ToDeviceEvent
import net.folivo.trixnity.core.model.events.m.DirectEventContent
import net.folivo.trixnity.core.model.events.m.key.verification.VerificationMethod
import net.folivo.trixnity.core.model.events.m.key.verification.VerificationMethod.Sas
import net.folivo.trixnity.core.model.events.m.key.verification.VerificationRequestToDeviceEventContent
import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent.VerificationRequest
import net.folivo.trixnity.core.model.events.m.secretstorage.DefaultSecretKeyEventContent
import net.folivo.trixnity.core.model.events.m.secretstorage.SecretKeyEventContent
import net.folivo.trixnity.core.model.events.m.secretstorage.SecretKeyEventContent.AesHmacSha2Key
import net.folivo.trixnity.core.subscribeContent
import net.folivo.trixnity.core.unsubscribeOnCompletion
import net.folivo.trixnity.crypto.core.SecureRandom
import net.folivo.trixnity.crypto.olm.DecryptedOlmEventContainer
import net.folivo.trixnity.crypto.olm.OlmDecrypter
import net.folivo.trixnity.crypto.olm.OlmEncryptionService
import net.folivo.trixnity.utils.nextString
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

private val log = KotlinLogging.logger {}

interface VerificationService {
    val activeDeviceVerification: StateFlow

    suspend fun createDeviceVerificationRequest(
        theirUserId: UserId,
        theirDeviceIds: Set
    ): Result

    suspend fun createUserVerificationRequest(
        theirUserId: UserId
    ): Result

    /**
     * Possible states include:
     * * [SelfVerificationMethods.PreconditionsNotMet]
     * * [SelfVerificationMethods.NoCrossSigningEnabled]
     * * [SelfVerificationMethods.AlreadyCrossSigned]
     * * [SelfVerificationMethods.CrossSigningEnabled]
     */
    sealed interface SelfVerificationMethods {
        /**
         * We don't have enough information yet to calculated available methods (e.g. waiting for the first sync).
         */
        data class PreconditionsNotMet(val reasons: Set) : SelfVerificationMethods {
            interface Reason {
                data object SyncNotRunning : Reason
                data object DeviceKeysNotFetchedYet : Reason
                data object CrossSigningKeysNotFetchedYet : Reason
            }
        }

        /**
         * Cross signing can be bootstrapped.
         * Bootstrapping can be done with [KeyService::bootstrapCrossSigning][net.folivo.trixnity.client.key.KeyServiceImpl.bootstrapCrossSigning].
         */
        data object NoCrossSigningEnabled : SelfVerificationMethods

        /**
         * No self verification needed.
         */
        data object AlreadyCrossSigned : SelfVerificationMethods

        /**
         * If empty: no other device & no key backup -> consider new bootstrapping of cross signing
         */
        data class CrossSigningEnabled(val methods: Set) : SelfVerificationMethods
    }

    fun getSelfVerificationMethods(): Flow

    @Deprecated(
        "use eventId instead",
        ReplaceWith("getActiveUserVerification(timelineEvent.roomId, timelineEvent.eventId)")
    )
    suspend fun getActiveUserVerification(
        timelineEvent: TimelineEvent,
    ): ActiveUserVerification?

    suspend fun getActiveUserVerification(
        roomId: RoomId,
        eventId: EventId,
    ): ActiveUserVerification?
}

class VerificationServiceImpl(
    userInfo: UserInfo,
    private val api: MatrixClientServerApiClient,
    private val keyStore: KeyStore,
    private val globalAccountDataStore: GlobalAccountDataStore,
    private val olmDecrypter: OlmDecrypter,
    private val olmEncryptionService: OlmEncryptionService,
    private val roomService: RoomService,
    private val keyService: KeyService,
    private val keyTrustService: KeyTrustService,
    private val keySecretService: KeySecretService,
    private val currentSyncState: CurrentSyncState,
    private val clock: Clock,
) : VerificationService, EventHandler {
    private val ownUserId = userInfo.userId
    private val ownDeviceId = userInfo.deviceId
    private val _activeDeviceVerification = MutableStateFlow(null)
    override val activeDeviceVerification = _activeDeviceVerification.asStateFlow()
    private val activeUserVerifications = MutableStateFlow>(listOf())
    private val supportedMethods: Set = setOf(Sas)

    override fun startInCoroutineScope(scope: CoroutineScope) {
        api.sync.subscribeContent(subscriber = ::handleDeviceVerificationRequestEvents)
            .unsubscribeOnCompletion(scope)
        olmDecrypter.subscribe(::handleOlmDecryptedDeviceVerificationRequestEvents)
            .unsubscribeOnCompletion(scope)
        // we use UNDISPATCHED because we want to ensure, that collect is called immediately
        scope.launch(start = UNDISPATCHED) {
            activeUserVerifications.collect { startLifecycleOfActiveVerifications(it, this) }
        }
        scope.launch(start = UNDISPATCHED) {
            activeDeviceVerification.collect { it?.let { startLifecycleOfActiveVerifications(listOf(it), this) } }
        }
    }

    private suspend fun handleDeviceVerificationRequestEvents(event: ClientEvent) {
        val content = event.content
        when (event) {
            is ToDeviceEvent -> {
                if (isVerificationRequestActive(content.timestamp, clock)) {
                    log.info { "got new device verification request from ${event.sender}" }
                    if (_activeDeviceVerification.value != null) {
                        log.info { "already have an active device verification -> cancelling new verification request" }
                        ActiveDeviceVerificationImpl(
                            request = event.content,
                            requestIsOurs = false,
                            ownUserId = ownUserId,
                            ownDeviceId = ownDeviceId,
                            theirUserId = event.sender,
                            theirDeviceId = content.fromDevice,
                            supportedMethods = supportedMethods,
                            api = api,
                            olmDecrypter = olmDecrypter,
                            olmEncryptionService = olmEncryptionService,
                            keyStore = keyStore,
                            keyTrust = keyTrustService,
                            clock = clock,
                        ).cancel()
                    } else {
                        _activeDeviceVerification.value =
                            ActiveDeviceVerificationImpl(
                                request = event.content,
                                requestIsOurs = false,
                                ownUserId = ownUserId,
                                ownDeviceId = ownDeviceId,
                                theirUserId = event.sender,
                                theirDeviceId = content.fromDevice,
                                supportedMethods = supportedMethods,
                                api = api,
                                olmDecrypter = olmDecrypter,
                                olmEncryptionService = olmEncryptionService,
                                keyTrust = keyTrustService,
                                keyStore = keyStore,
                                clock = clock,
                            )
                    }
                } else {
                    log.warn { "Received device verification request that is not active anymore: $event" }
                }
            }

            else -> log.warn { "got new device verification request with an event type ${event::class.simpleName}, that we did not expected" }
        }
    }

    private suspend fun handleOlmDecryptedDeviceVerificationRequestEvents(event: DecryptedOlmEventContainer) {
        when (val content = event.decrypted.content) {
            is VerificationRequestToDeviceEventContent -> {
                if (isVerificationRequestActive(content.timestamp, clock)) {
                    log.info { "got new device verification request from ${event.decrypted.sender}" }
                    if (_activeDeviceVerification.value != null) {
                        log.info { "already have an active device verification -> cancelling new verification request" }
                        ActiveDeviceVerificationImpl(
                            request = content,
                            requestIsOurs = false,
                            ownUserId = ownUserId,
                            ownDeviceId = ownDeviceId,
                            theirUserId = event.decrypted.sender,
                            theirDeviceId = content.fromDevice,
                            supportedMethods = supportedMethods,
                            api = api,
                            olmDecrypter = olmDecrypter,
                            olmEncryptionService = olmEncryptionService,
                            keyTrust = keyTrustService,
                            keyStore = keyStore,
                            clock = clock,
                        ).cancel("already have an active device verification")
                    } else {
                        _activeDeviceVerification.value =
                            ActiveDeviceVerificationImpl(
                                request = content,
                                requestIsOurs = false,
                                ownUserId = ownUserId,
                                ownDeviceId = ownDeviceId,
                                theirUserId = event.decrypted.sender,
                                theirDeviceId = content.fromDevice,
                                supportedMethods = supportedMethods,
                                api = api,
                                olmDecrypter = olmDecrypter,
                                olmEncryptionService = olmEncryptionService,
                                keyTrust = keyTrustService,
                                keyStore = keyStore,
                                clock = clock,
                            )
                    }
                }
            }

            else -> {}
        }
    }

    private suspend fun startLifecycleOfActiveVerifications(
        verifications: List,
        scope: CoroutineScope
    ) {
        verifications.forEach { verification ->
            val started = verification.startLifecycle(scope)
            if (started)
                scope.launch {
                    verification.state.first { verification.state.value is Done || verification.state.value is Cancel }
                    when (verification) {
                        is ActiveUserVerificationImpl -> {
                            delay(20.minutes)
                            activeUserVerifications.update { it - verification }
                        }

                        is ActiveDeviceVerificationImpl -> {
                            _activeDeviceVerification.update { null }
                        }
                    }
                }
        }
    }

    override suspend fun createDeviceVerificationRequest(
        theirUserId: UserId,
        theirDeviceIds: Set
    ): Result = kotlin.runCatching {
        log.info { "create new device verification request to $theirUserId ($theirDeviceIds)" }
        val request = VerificationRequestToDeviceEventContent(
            ownDeviceId, supportedMethods, clock.now().toEpochMilliseconds(), SecureRandom.nextString(22)
        )
        api.user.sendToDevice(mapOf(theirUserId to theirDeviceIds.toSet().associateWith {
            olmEncryptionService.encryptOlm(request, theirUserId, it).getOrNull() ?: request
        })).getOrThrow()
        ActiveDeviceVerificationImpl(
            request = request,
            requestIsOurs = true,
            ownUserId = ownUserId,
            ownDeviceId = ownDeviceId,
            theirUserId = theirUserId,
            theirDeviceIds = theirDeviceIds.toSet(),
            supportedMethods = supportedMethods,
            api = api,
            olmDecrypter = olmDecrypter,
            olmEncryptionService = olmEncryptionService,
            keyTrust = keyTrustService,
            keyStore = keyStore,
            clock = clock,
        ).also { newDeviceVerification ->
            _activeDeviceVerification.getAndUpdate { newDeviceVerification }?.cancel()
        }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    override suspend fun createUserVerificationRequest(
        theirUserId: UserId
    ): Result = kotlin.runCatching {
        coroutineScope {
            log.info { "create new user verification request to $theirUserId" }
            val request = VerificationRequest(ownDeviceId, theirUserId, supportedMethods)
            val roomId =
                globalAccountDataStore.get().first()?.content?.mappings?.get(theirUserId)
                    ?.firstOrNull()
                    ?: api.room.createRoom(invite = setOf(theirUserId), isDirect = true).getOrThrow()
            val transactionId = roomService.sendMessage(roomId) {
                content(request)
            }
            val eventId = roomService.getOutbox()
                .flatMapLatest { it[transactionId] ?: flowOf(null) }
                .mapNotNull { it?.eventId }
                .first()
            ActiveUserVerificationImpl(
                request = request,
                requestIsFromOurOwn = true,
                requestEventId = eventId,
                requestTimestamp = clock.now().toEpochMilliseconds(),
                ownUserId = ownUserId,
                ownDeviceId = ownDeviceId,
                theirUserId = theirUserId,
                theirInitialDeviceId = null,
                roomId = roomId,
                supportedMethods = supportedMethods,
                json = api.json,
                keyStore = keyStore,
                room = roomService,
                keyTrust = keyTrustService,
                clock = clock,
            ).also { auv -> activeUserVerifications.update { it + auv } }
        }
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    override fun getSelfVerificationMethods(): Flow {
        return combine(
            keyService.bootstrapRunning,
            currentSyncState,
            keyStore.getCrossSigningKeys(ownUserId),
            keyStore.getDeviceKeys(ownUserId),
            globalAccountDataStore.get()
                .transformLatest { event ->
                    event?.content?.key?.let {
                        emitAll(globalAccountDataStore.get(it))
                    } ?: emit(null)
                },
        ) { bootstrapRunning, currentSyncState, crossSigningKeys, deviceKeys, defaultKey ->
            log.trace {
                "self verification preconditions: bootstrapRunning=$bootstrapRunning currentSyncState=$currentSyncState " +
                        "crossSigningKeys=$crossSigningKeys deviceKeys=$deviceKeys defaultKey=$defaultKey"
            }
            // preconditions: sync running, login was successful and we are not yet cross-signed
            if (currentSyncState != SyncState.RUNNING || deviceKeys == null || crossSigningKeys == null)
                return@combine PreconditionsNotMet(
                    setOfNotNull(
                        if (currentSyncState != SyncState.RUNNING) PreconditionsNotMet.Reason.SyncNotRunning else null,
                        if (deviceKeys == null) PreconditionsNotMet.Reason.DeviceKeysNotFetchedYet else null,
                        if (crossSigningKeys == null) PreconditionsNotMet.Reason.CrossSigningKeysNotFetchedYet else null
                    )
                )
            val ownTrustLevel = deviceKeys[ownDeviceId]?.trustLevel
            if (ownTrustLevel == KeySignatureTrustLevel.CrossSigned(true)) return@combine SelfVerificationMethods.AlreadyCrossSigned

            // we need bootstrapping if this is the first device or bootstrapping is in progress
            if (crossSigningKeys.isEmpty()) return@combine SelfVerificationMethods.NoCrossSigningEnabled
            if (bootstrapRunning) return@combine SelfVerificationMethods.NoCrossSigningEnabled

            val deviceVerificationMethod = deviceKeys.entries
                .filter { it.value.trustLevel is KeySignatureTrustLevel.CrossSigned }
                .map { it.key }
                .let {
                    val sendToDevices = it - ownDeviceId
                    if (sendToDevices.isNotEmpty())
                        setOf(
                            SelfVerificationMethod.CrossSignedDeviceVerification(
                                ownUserId,
                                sendToDevices.toSet(),
                                ::createDeviceVerificationRequest
                            )
                        )
                    else setOf()
                }

            val recoveryKeyMethods = when (val content = defaultKey?.content) {
                is AesHmacSha2Key -> when (content.passphrase) {
                    is AesHmacSha2Key.SecretStorageKeyPassphrase.Pbkdf2 ->
                        setOf(
                            SelfVerificationMethod.AesHmacSha2RecoveryKeyWithPbkdf2Passphrase(
                                keySecretService,
                                keyTrustService,
                                defaultKey.key,
                                content
                            ),
                            SelfVerificationMethod.AesHmacSha2RecoveryKey(
                                keySecretService,
                                keyTrustService,
                                defaultKey.key,
                                content
                            )
                        )

                    is AesHmacSha2Key.SecretStorageKeyPassphrase.Unknown, null ->
                        setOf(
                            SelfVerificationMethod.AesHmacSha2RecoveryKey(
                                keySecretService,
                                keyTrustService,
                                defaultKey.key,
                                content
                            )
                        )
                }

                is SecretKeyEventContent.Unknown, null -> setOf()
            }

            return@combine SelfVerificationMethods.CrossSigningEnabled(recoveryKeyMethods + deviceVerificationMethod)
        }
    }

    private val getActiveUserVerificationMutex = Mutex()

    @Deprecated(
        "use eventId instead",
        replaceWith = ReplaceWith("getActiveUserVerification(timelineEvent.roomId, timelineEvent.eventId)")
    )
    override suspend fun getActiveUserVerification(
        timelineEvent: TimelineEvent,
    ): ActiveUserVerification? = getActiveUserVerification(timelineEvent.roomId, timelineEvent.eventId)

    override suspend fun getActiveUserVerification(
        roomId: RoomId,
        eventId: EventId,
    ): ActiveUserVerification? {
        val timelineEvent =
            withTimeoutOrNull(6.seconds) {
                roomService.getTimelineEvent(roomId, eventId) { decryptionTimeout = 5.seconds }
                    .filter { it?.content != null }.first()
            } ?: return null
        val request = timelineEvent.content?.getOrNull() as? VerificationRequest ?: return null
        return if (isVerificationRequestActive(timelineEvent.event.originTimestamp, clock)) {
            getActiveUserVerificationMutex.withLock {
                val cache =
                    activeUserVerifications.value.find { it.roomId == roomId && it.relatesTo?.eventId == eventId }
                if (cache != null) cache
                else {
                    val sender = timelineEvent.event.sender
                    if (sender != ownUserId && request.to == ownUserId) {
                        ActiveUserVerificationImpl(
                            request = request,
                            requestIsFromOurOwn = false,
                            requestEventId = eventId,
                            requestTimestamp = timelineEvent.event.originTimestamp,
                            ownUserId = ownUserId,
                            ownDeviceId = ownDeviceId,
                            theirUserId = sender,
                            theirInitialDeviceId = request.fromDevice,
                            roomId = roomId,
                            supportedMethods = supportedMethods,
                            json = api.json,
                            keyStore = keyStore,
                            room = roomService,
                            keyTrust = keyTrustService,
                            clock = clock,
                        ).also { auv -> activeUserVerifications.update { it + auv } }
                    } else null
                }
            }
        } else null
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy