From 098c9fe2aa99a0983296a5d9ba65b7ed528fb591 Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Fri, 8 May 2026 07:03:27 +0300 Subject: [PATCH] feat: device verification --- .../pixeldragon/ui/layout/Settings.kt | 717 +++++++++++++++++- 1 file changed, 701 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Settings.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Settings.kt index 1addec6..d2d9339 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Settings.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Settings.kt @@ -64,8 +64,16 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import de.connect2x.trixnity.client.MatrixClient +import de.connect2x.trixnity.client.key import de.connect2x.trixnity.client.store.KeySignatureTrustLevel import de.connect2x.trixnity.client.store.KeyStore +import de.connect2x.trixnity.client.verification +import de.connect2x.trixnity.client.verification.ActiveDeviceVerification +import de.connect2x.trixnity.client.verification.ActiveSasVerificationMethod +import de.connect2x.trixnity.client.verification.ActiveSasVerificationState +import de.connect2x.trixnity.client.verification.ActiveVerificationState +import de.connect2x.trixnity.client.verification.SelfVerificationMethod +import de.connect2x.trixnity.client.verification.VerificationService import de.connect2x.trixnity.clientserverapi.client.SyncState import de.connect2x.trixnity.clientserverapi.client.UIA import de.connect2x.trixnity.clientserverapi.model.authentication.IdentifierType @@ -75,6 +83,7 @@ import de.connect2x.trixnity.clientserverapi.model.uia.AuthenticationRequest import de.connect2x.trixnity.clientserverapi.model.user.ProfileField import de.connect2x.trixnity.clientserverapi.model.user.avatarUrl import de.connect2x.trixnity.clientserverapi.model.user.displayName +import de.connect2x.trixnity.core.model.events.m.key.verification.VerificationMethod import io.ktor.http.ContentType import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.flow.Flow @@ -452,6 +461,13 @@ fun AccountSettingsScreen(modifier: Modifier = Modifier) { fun DevicesSettingsScreen(modifier: Modifier = Modifier) { val matrixClient = client!! val syncState by matrixClient.api.sync.currentSyncState.collectAsState(initial = SyncState.STOPPED) + val selfVerificationMethodsFlow = remember(matrixClient) { + matrixClient.verification.getSelfVerificationMethods() + } + val selfVerificationMethods by selfVerificationMethodsFlow.collectAsState( + initial = VerificationService.SelfVerificationMethods.PreconditionsNotMet(emptySet()), + ) + val activeDeviceVerification by matrixClient.verification.activeDeviceVerification.collectAsState() val scope = rememberCoroutineScope() val manufacturer = Build.MANUFACTURER.replaceFirstChar { it.titlecase() } val deviceName = listOf(manufacturer, Build.MODEL) @@ -461,8 +477,18 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { var sessions by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(true) } var isClosingDeviceId by remember { mutableStateOf(null) } + var isStartingVerificationDeviceId by remember { mutableStateOf(null) } + var isStartingSelfVerification by remember { mutableStateOf(false) } + var isBootstrapping by remember { mutableStateOf(false) } + var isAuthenticating by remember { mutableStateOf(false) } var message by remember { mutableStateOf(null) } var pendingAuthentication by remember { mutableStateOf(null) } + var bootstrapDialogOpen by remember { mutableStateOf(false) } + var recoveryKeyToShow by remember { mutableStateOf(null) } + var recoveryKeyMethod by remember { mutableStateOf(null) } + var recoveryPassphraseMethod by remember { + mutableStateOf(null) + } suspend fun loadSessions() { isLoading = true @@ -477,6 +503,108 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { isLoading = false } + fun completeBootstrap(recoveryKey: String) { + recoveryKeyToShow = recoveryKey + message = "Cross-signing is set up. Save the recovery key now — PixelDragon will not show it again." + scope.launch { loadSessions() } + } + + fun bootstrapCrossSigning(passphrase: String?) { + scope.launch { + isBootstrapping = true + message = null + val bootstrap = runCatching { + if (passphrase.isNullOrBlank()) { + matrixClient.key.bootstrapCrossSigning() + } else { + matrixClient.key.bootstrapCrossSigningFromPassphrase(passphrase) + } + }.getOrElse { error -> + message = error.toSettingsError() + isBootstrapping = false + return@launch + } + + bootstrap.result.onSuccess { uia -> + uia.handleUia( + onNeedsAuthentication = { step -> + pendingAuthentication = PendingAuthentication( + title = "Set up encryption recovery", + description = "Enter your current password to upload cross-signing keys and verify this session.", + uia = step, + onSuccess = { completeBootstrap(bootstrap.recoveryKey) }, + ) + message = "Password confirmation is required to set up cross-signing." + }, + onSuccess = { completeBootstrap(bootstrap.recoveryKey) }, + ) + }.onFailure { error -> + message = error.toSettingsError() + } + isBootstrapping = false + } + } + + fun startSelfVerification(method: SelfVerificationMethod.CrossSignedDeviceVerification) { + scope.launch { + isStartingSelfVerification = true + message = null + method.createDeviceVerification() + .onSuccess { message = "Verification request sent to your verified session." } + .onFailure { message = it.toSettingsError() } + isStartingSelfVerification = false + } + } + + fun verifyWithRecoveryKey(method: SelfVerificationMethod.AesHmacSha2RecoveryKey, recoveryKey: String) { + scope.launch { + isStartingSelfVerification = true + message = null + method.verify(recoveryKey) + .onSuccess { + recoveryKeyMethod = null + message = "This session is verified." + loadSessions() + } + .onFailure { message = it.toSettingsError() } + isStartingSelfVerification = false + } + } + + fun verifyWithRecoveryPassphrase( + method: SelfVerificationMethod.AesHmacSha2RecoveryKeyWithPbkdf2Passphrase, + passphrase: String, + ) { + scope.launch { + isStartingSelfVerification = true + message = null + method.verify(passphrase) + .onSuccess { + recoveryPassphraseMethod = null + message = "This session is verified." + loadSessions() + } + .onFailure { message = it.toSettingsError() } + isStartingSelfVerification = false + } + } + + fun startDeviceVerification(device: Device) { + scope.launch { + isStartingVerificationDeviceId = device.deviceId + message = null + matrixClient.verification.createDeviceVerificationRequest( + theirUserId = matrixClient.userId, + theirDeviceIds = setOf(device.deviceId), + ).onSuccess { + message = "Verification request sent to ${device.displayName ?: device.deviceId}." + }.onFailure { + message = it.toSettingsError() + } + isStartingVerificationDeviceId = null + } + } + fun closeSession(device: Device) { scope.launch { isClosingDeviceId = device.deviceId @@ -518,14 +646,12 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { item { SettingsHeader( title = "Devices", - subtitle = "All Matrix sessions connected to your account.", + subtitle = "Verify sessions so encrypted messages can be sent safely.", ) } message?.let { status -> - item { - SettingsStatusMessage(status) - } + item { SettingsStatusMessage(status) } } item { @@ -540,6 +666,16 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { value = matrixClient.deviceId, ) SettingsDivider() + SettingsInfoRow( + title = "Identity key", + value = matrixClient.identityKey.value.value, + ) + SettingsDivider() + SettingsInfoRow( + title = "Signing key", + value = matrixClient.signingKey.value.value, + ) + SettingsDivider() SettingsInfoRow( title = "Android", value = "${Build.VERSION.RELEASE} · API ${Build.VERSION.SDK_INT}", @@ -552,6 +688,90 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { } } + item { + SettingsSection(title = "Self-verification") { + SettingsInfoRow( + title = "Status", + value = selfVerificationMethods.toSelfVerificationStatus(), + ) + SettingsDivider() + when (val methods = selfVerificationMethods) { + is VerificationService.SelfVerificationMethods.PreconditionsNotMet -> { + SettingsInfoRow( + title = "Waiting for", + value = methods.reasons.joinToString { it.toSelfVerificationReason() } + .ifBlank { "Initial sync" }, + ) + } + + VerificationService.SelfVerificationMethods.NoCrossSigningEnabled -> { + SettingsActionRow( + title = "Set up cross-signing", + value = "Create recovery key, key backup and verify this session", + actionLabel = "Set up", + enabled = !isBootstrapping, + onClick = { bootstrapDialogOpen = true }, + ) + } + + VerificationService.SelfVerificationMethods.AlreadyCrossSigned -> { + SettingsInfoRow( + title = "This session", + value = "Cross-signed and verified", + ) + } + + is VerificationService.SelfVerificationMethods.CrossSigningEnabled -> { + if (methods.methods.isEmpty()) { + SettingsActionRow( + title = "Reset cross-signing", + value = "No verified session or recovery method is available", + actionLabel = "Set up", + enabled = !isBootstrapping, + destructive = true, + onClick = { bootstrapDialogOpen = true }, + ) + } else { + methods.methods.forEachIndexed { index, method -> + if (index > 0) SettingsDivider() + when (method) { + is SelfVerificationMethod.CrossSignedDeviceVerification -> { + SettingsActionRow( + title = "Verify from another session", + value = "Sends SAS/emoji request to your already verified devices", + actionLabel = "Start", + enabled = !isStartingSelfVerification, + onClick = { startSelfVerification(method) }, + ) + } + + is SelfVerificationMethod.AesHmacSha2RecoveryKey -> { + SettingsActionRow( + title = "Verify with recovery key", + value = "Use your Matrix recovery key", + actionLabel = "Enter", + enabled = !isStartingSelfVerification, + onClick = { recoveryKeyMethod = method }, + ) + } + + is SelfVerificationMethod.AesHmacSha2RecoveryKeyWithPbkdf2Passphrase -> { + SettingsActionRow( + title = "Verify with recovery passphrase", + value = "Use your Matrix security phrase", + actionLabel = "Enter", + enabled = !isStartingSelfVerification, + onClick = { recoveryPassphraseMethod = method }, + ) + } + } + } + } + } + } + } + } + item { Row( modifier = Modifier @@ -567,9 +787,7 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { ) TextButton( enabled = !isLoading, - onClick = { - scope.launch { loadSessions() } - }, + onClick = { scope.launch { loadSessions() } }, ) { Text("Refresh") } @@ -591,9 +809,7 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { } sessions.isEmpty() -> { - item { - SettingsStatusMessage("No active sessions returned by the homeserver") - } + item { SettingsStatusMessage("No active sessions returned by the homeserver") } } else -> { @@ -605,24 +821,80 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { session = session, currentDeviceId = matrixClient.deviceId, isClosing = isClosingDeviceId == session.device.deviceId, + isStartingVerification = isStartingVerificationDeviceId == session.device.deviceId, onClose = { closeSession(session.device) }, + onVerify = { startDeviceVerification(session.device) }, ) } } } } + activeDeviceVerification?.let { verification -> + DeviceVerificationDialog( + verification = verification, + onDismiss = { + scope.launch { verification.cancel() } + }, + onDone = { + scope.launch { loadSessions() } + }, + ) + } + + if (bootstrapDialogOpen) { + BootstrapCrossSigningDialog( + isBusy = isBootstrapping, + onDismiss = { bootstrapDialogOpen = false }, + onBootstrap = { passphrase -> + bootstrapDialogOpen = false + bootstrapCrossSigning(passphrase) + }, + ) + } + + recoveryKeyToShow?.let { recoveryKey -> + RecoveryKeyResultDialog( + recoveryKey = recoveryKey, + onDismiss = { recoveryKeyToShow = null }, + ) + } + + recoveryKeyMethod?.let { method -> + RecoverySecretDialog( + title = "Verify with recovery key", + label = "Recovery key", + isPassword = false, + isBusy = isStartingSelfVerification, + onDismiss = { recoveryKeyMethod = null }, + onConfirm = { recoveryKey -> verifyWithRecoveryKey(method, recoveryKey) }, + ) + } + + recoveryPassphraseMethod?.let { method -> + RecoverySecretDialog( + title = "Verify with recovery passphrase", + label = "Security phrase", + isPassword = true, + isBusy = isStartingSelfVerification, + onDismiss = { recoveryPassphraseMethod = null }, + onConfirm = { passphrase -> verifyWithRecoveryPassphrase(method, passphrase) }, + ) + } + pendingAuthentication?.let { pending -> PasswordAuthenticationDialog( title = pending.title, description = pending.description, - isBusy = isClosingDeviceId != null, + isBusy = isAuthenticating, onDismiss = { pendingAuthentication = null isClosingDeviceId = null + isAuthenticating = false }, onConfirm = { password -> scope.launch { + isAuthenticating = true val authRequest = AuthenticationRequest.Password( identifier = IdentifierType.User(matrixClient.userId.toString()), password = password, @@ -646,6 +918,7 @@ fun DevicesSettingsScreen(modifier: Modifier = Modifier) { }.onFailure { message = it.toSettingsError() } + isAuthenticating = false isClosingDeviceId = null } }, @@ -1123,10 +1396,13 @@ private fun SessionCard( session: MatrixSession, currentDeviceId: String, isClosing: Boolean, + isStartingVerification: Boolean, onClose: () -> Unit, + onVerify: () -> Unit, ) { val device = session.device val isCurrent = device.deviceId == currentDeviceId + val verified = session.verification.isVerified() Surface( modifier = Modifier @@ -1192,18 +1468,384 @@ private fun SessionCard( Spacer(Modifier.height(12.dp)) - OutlinedButton( - enabled = !isCurrent && !isClosing, - onClick = onClose, + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - if (isClosing) { + OutlinedButton( + enabled = !isCurrent && !verified && !isStartingVerification, + onClick = onVerify, + ) { + if (isStartingVerification) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + } + Text(if (verified) "Verified" else "Verify") + } + + OutlinedButton( + enabled = !isCurrent && !isClosing, + onClick = onClose, + ) { + if (isClosing) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + } + Text(if (isCurrent) "Current session" else "Close") + } + } + } + } +} + +@Composable +private fun BootstrapCrossSigningDialog( + isBusy: Boolean, + onDismiss: () -> Unit, + onBootstrap: (String?) -> Unit, +) { + var usePassphrase by remember { mutableStateOf(false) } + var passphrase by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Set up encryption recovery") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "This creates cross-signing keys, verifies this session, and creates a Matrix recovery key. Save the recovery key somewhere safe.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = usePassphrase, + onCheckedChange = { usePassphrase = it }, + ) + Spacer(Modifier.width(8.dp)) + Text("Protect recovery with a passphrase") + } + if (usePassphrase) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = passphrase, + onValueChange = { passphrase = it }, + label = { Text("Security phrase") }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + singleLine = true, + ) + } + } + }, + confirmButton = { + Button( + enabled = !isBusy && (!usePassphrase || passphrase.isNotBlank()), + onClick = { onBootstrap(passphrase.takeIf { usePassphrase }) }, + ) { + if (isBusy) { CircularProgressIndicator( modifier = Modifier.size(18.dp), strokeWidth = 2.dp, ) Spacer(Modifier.width(8.dp)) } - Text(if (isCurrent) "Current session" else "Close session") + Text("Set up") + } + }, + dismissButton = { + TextButton( + enabled = !isBusy, + onClick = onDismiss, + ) { + Text("Cancel") + } + }, + ) +} + +@Composable +private fun RecoveryKeyResultDialog( + recoveryKey: String, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Save your recovery key") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "You will need this key to verify future sessions and restore encrypted message keys.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Text( + modifier = Modifier.padding(12.dp), + text = recoveryKey, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + }, + confirmButton = { + Button(onClick = onDismiss) { + Text("I saved it") + } + }, + ) +} + +@Composable +private fun RecoverySecretDialog( + title: String, + label: String, + isPassword: Boolean, + isBusy: Boolean, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit, +) { + var value by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = value, + onValueChange = { value = it }, + label = { Text(label) }, + visualTransformation = if (isPassword) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None, + keyboardOptions = if (isPassword) KeyboardOptions(keyboardType = KeyboardType.Password) else KeyboardOptions.Default, + singleLine = true, + ) + }, + confirmButton = { + Button( + enabled = value.isNotBlank() && !isBusy, + onClick = { onConfirm(value) }, + ) { + if (isBusy) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + } + Text("Verify") + } + }, + dismissButton = { + TextButton( + enabled = !isBusy, + onClick = onDismiss, + ) { + Text("Cancel") + } + }, + ) +} + +@Composable +private fun DeviceVerificationDialog( + verification: ActiveDeviceVerification, + onDismiss: () -> Unit, + onDone: () -> Unit, +) { + val scope = rememberCoroutineScope() + val state by verification.state.collectAsState() + + LaunchedEffect(state) { + if (state is ActiveVerificationState.Done) { + onDone() + } + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Verify session") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + SettingsTinyInfoRow( + title = "User", + value = verification.theirUserId.toString(), + ) + SettingsTinyInfoRow( + title = "Device", + value = verification.theirDeviceId ?: "Waiting for device", + ) + Text( + text = state.toVerificationStateDescription(), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + when (val currentState = state) { + is ActiveVerificationState.Ready -> { + SettingsStatusMessage( + message = "Both devices accepted the request. Start SAS verification and compare emojis on both devices.", + ) + } + + is ActiveVerificationState.Start -> { + val sas = currentState.method as? ActiveSasVerificationMethod + if (sas != null) { + SasVerificationContent(sas) + } else { + SettingsStatusMessage("Unsupported verification method") + } + } + + is ActiveVerificationState.Cancel -> { + SettingsStatusMessage("Cancelled: ${currentState.content.reason}") + } + + else -> Unit + } + } + }, + confirmButton = { + VerificationPrimaryAction( + state = state, + onClose = onDismiss, + ) + }, + dismissButton = { + TextButton( + onClick = { + scope.launch { verification.cancel() } + onDismiss() + }, + ) { + Text(if (state is ActiveVerificationState.Done) "Close" else "Cancel") + } + }, + ) +} + +@Composable +private fun VerificationPrimaryAction( + state: ActiveVerificationState, + onClose: () -> Unit, +) { + val scope = rememberCoroutineScope() + + when (state) { + is ActiveVerificationState.TheirRequest -> { + Button(onClick = { scope.launch { state.ready() } }) { + Text("Accept") + } + } + + is ActiveVerificationState.Ready -> { + Button( + enabled = state.methods.contains(VerificationMethod.Sas), + onClick = { scope.launch { state.start(VerificationMethod.Sas) } }, + ) { + Text("Start SAS") + } + } + + is ActiveVerificationState.Start -> { + val sas = state.method as? ActiveSasVerificationMethod + if (sas == null) { + Button(enabled = false, onClick = {}) { + Text("Unsupported") + } + } else { + val sasState by sas.state.collectAsState() + when (val currentSasState = sasState) { + is ActiveSasVerificationState.TheirSasStart -> { + Button(onClick = { scope.launch { currentSasState.accept() } }) { + Text("Accept SAS") + } + } + + is ActiveSasVerificationState.ComparisonByUser -> { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TextButton(onClick = { scope.launch { currentSasState.noMatch() } }) { + Text("No match") + } + Button(onClick = { scope.launch { currentSasState.match() } }) { + Text("They match") + } + } + } + + else -> { + Button(enabled = false, onClick = {}) { + Text("Waiting") + } + } + } + } + } + + is ActiveVerificationState.Done, + is ActiveVerificationState.Cancel, + ActiveVerificationState.AcceptedByOtherDevice, + ActiveVerificationState.Undefined -> { + Button(onClick = onClose) { + Text("Close") + } + } + + is ActiveVerificationState.OwnRequest, + is ActiveVerificationState.WaitForDone -> { + Button(enabled = false, onClick = {}) { + Text("Waiting") + } + } + } +} + +@Composable +private fun SasVerificationContent(sas: ActiveSasVerificationMethod) { + val sasState by sas.state.collectAsState() + + when (val currentSasState = sasState) { + is ActiveSasVerificationState.ComparisonByUser -> { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + text = "Compare these emojis with the other device. Only tap “They match” if both sides show the same sequence.", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = currentSasState.emojis.joinToString(" ") { it.second }, + style = MaterialTheme.typography.headlineSmall, + ) + Text( + text = currentSasState.decimal.joinToString(" · "), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + + is ActiveSasVerificationState.TheirSasStart -> { + SettingsStatusMessage("The other device started SAS. Accept it to show emojis.") + } + + is ActiveSasVerificationState.OwnSasStart, + is ActiveSasVerificationState.Accept, + is ActiveSasVerificationState.WaitForKeys, + ActiveSasVerificationState.WaitForMacs -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + CircularProgressIndicator() } } } @@ -1565,6 +2207,49 @@ private suspend fun UIA.handleUia( } } +private fun KeySignatureTrustLevel?.isVerified(): Boolean { + return when (this) { + is KeySignatureTrustLevel.Valid -> verified + is KeySignatureTrustLevel.CrossSigned -> verified + is KeySignatureTrustLevel.NotAllDeviceKeysCrossSigned -> verified + else -> false + } +} + +private fun VerificationService.SelfVerificationMethods.toSelfVerificationStatus(): String { + return when (this) { + is VerificationService.SelfVerificationMethods.PreconditionsNotMet -> "Waiting for sync and keys" + VerificationService.SelfVerificationMethods.NoCrossSigningEnabled -> "Cross-signing is not set up" + VerificationService.SelfVerificationMethods.AlreadyCrossSigned -> "This session is verified" + is VerificationService.SelfVerificationMethods.CrossSigningEnabled -> { + if (methods.isEmpty()) "Recovery unavailable" else "Verification available" + } + } +} + +private fun VerificationService.SelfVerificationMethods.PreconditionsNotMet.Reason.toSelfVerificationReason(): String { + return when (this) { + VerificationService.SelfVerificationMethods.PreconditionsNotMet.Reason.SyncNotRunning -> "sync is not running" + VerificationService.SelfVerificationMethods.PreconditionsNotMet.Reason.DeviceKeysNotFetchedYet -> "device keys" + VerificationService.SelfVerificationMethods.PreconditionsNotMet.Reason.CrossSigningKeysNotFetchedYet -> "cross-signing keys" + else -> this::class.simpleName ?: "unknown" + } +} + +private fun ActiveVerificationState.toVerificationStateDescription(): String { + return when (this) { + is ActiveVerificationState.OwnRequest -> "Waiting for the other device to accept the verification request." + is ActiveVerificationState.TheirRequest -> "The other device wants to verify this session." + is ActiveVerificationState.Ready -> "Ready to start SAS verification." + is ActiveVerificationState.Start -> "SAS verification is in progress." + is ActiveVerificationState.WaitForDone -> "Waiting for the other device to finish." + ActiveVerificationState.Done -> "Verification completed." + is ActiveVerificationState.Cancel -> "Verification was cancelled." + ActiveVerificationState.AcceptedByOtherDevice -> "Another device accepted this request." + ActiveVerificationState.Undefined -> "Verification state is missing. Start again." + } +} + private fun KeySignatureTrustLevel?.toVerificationStatus(): String { return when (this) { is KeySignatureTrustLevel.Valid -> if (verified) "Verified" else "Valid, not verified"