feat: device verification

This commit is contained in:
2026-05-08 07:03:27 +03:00
parent 6bf33a91c9
commit 098c9fe2aa
@@ -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<List<MatrixSession>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var isClosingDeviceId by remember { mutableStateOf<String?>(null) }
var isStartingVerificationDeviceId by remember { mutableStateOf<String?>(null) }
var isStartingSelfVerification by remember { mutableStateOf(false) }
var isBootstrapping by remember { mutableStateOf(false) }
var isAuthenticating by remember { mutableStateOf(false) }
var message by remember { mutableStateOf<String?>(null) }
var pendingAuthentication by remember { mutableStateOf<PendingAuthentication?>(null) }
var bootstrapDialogOpen by remember { mutableStateOf(false) }
var recoveryKeyToShow by remember { mutableStateOf<String?>(null) }
var recoveryKeyMethod by remember { mutableStateOf<SelfVerificationMethod.AesHmacSha2RecoveryKey?>(null) }
var recoveryPassphraseMethod by remember {
mutableStateOf<SelfVerificationMethod.AesHmacSha2RecoveryKeyWithPbkdf2Passphrase?>(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<Unit>.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"