Self-check key transparency.

This commit is contained in:
Michelle Tang
2026-02-02 14:12:16 -05:00
committed by GitHub
parent 853a37920c
commit cd925d5f53
14 changed files with 507 additions and 10 deletions

View File

@@ -0,0 +1,194 @@
package org.thoughtcrime.securesms.verify
import android.content.DialogInterface
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
/**
* Sheet to prompt for debug logs when self key transparency fails
*/
class SelfVerificationFailureSheet : ComposeBottomSheetDialogFragment() {
private val viewModel: SelfVerificationFailureViewModel by viewModels()
override val peekHeightPercentage: Float = 0.75f
companion object {
@JvmStatic
fun show(fragmentManager: FragmentManager) {
SignalStore.misc.hasSeenKeyTransparencyFailure = true
SelfVerificationFailureSheet().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
}
@Composable
override fun SheetContent() {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
LaunchedEffect(state.sendEmail) {
if (state.sendEmail && state.debugLogUrl != null) {
val subject = context.getString(R.string.SelfVerificationFailureSheet__email_subject)
val prefix = "\n${context.getString(R.string.HelpFragment__debug_log)} ${state.debugLogUrl}\n\n"
val body = SupportEmailUtil.generateSupportEmailBody(context, R.string.SelfVerificationFailureSheet__email_filter, prefix, null)
CommunicationActions.openEmail(context, SupportEmailUtil.getSupportEmailAddress(context), subject, body)
dismissAllowingStateLoss()
} else if (state.sendEmail) {
Toast.makeText(requireContext(), getString(R.string.HelpFragment__could_not_upload_logs), Toast.LENGTH_LONG).show()
dismissAllowingStateLoss()
}
}
VerifyFailureSheet(
state,
onLearnMoreClicked = {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.HelpFragment__link__debug_info))
},
onDismiss = {
dismissAllowingStateLoss()
},
onSubmit = {
viewModel.submitLogs()
}
)
}
}
@Composable
fun VerifyFailureSheet(
state: VerificationUiState,
onLearnMoreClicked: () -> Unit = {},
onDismiss: () -> Unit = {},
onSubmit: () -> Unit = {}
) {
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.horizontalGutters()
) {
BottomSheets.Handle()
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_24),
contentDescription = null,
tint = Color(0xFFC88600),
modifier = Modifier
.padding(top = 24.dp, bottom = 8.dp)
.size(66.dp)
.background(color = Color(0xFFF9E4B6), shape = CircleShape)
.padding(12.dp)
)
Text(
text = stringResource(R.string.SelfVerificationFailureSheet__title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.padding(vertical = 12.dp),
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = buildAnnotatedString {
append(stringResource(id = R.string.SelfVerificationFailureSheet__body))
append(" ")
withLink(
LinkAnnotation.Clickable(tag = "learn-more") { onLearnMoreClicked() }
) {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.SelfVerificationFailureSheet__learn_more))
}
}
},
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 54.dp, bottom = 28.dp)
) {
Buttons.LargeTonal(
onClick = onDismiss,
modifier = Modifier.weight(1f)
) {
Text(stringResource(id = R.string.SelfVerificationFailureSheet__no_thanks))
}
Spacer(modifier = Modifier.size(12.dp))
Buttons.LargeTonal(
onClick = if (state.showAsProgress) {
{}
} else {
onSubmit
},
modifier = Modifier.weight(1f)
) {
if (state.showAsProgress) {
CircularProgressIndicator(
strokeWidth = 3.dp,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
} else {
Text(stringResource(id = R.string.SelfVerificationFailureSheet__submit))
}
}
}
}
}
@DayNightPreviews
@Composable
fun VerifyFailureSheetPreview() {
Previews.BottomSheetContentPreview {
VerifyFailureSheet(state = VerificationUiState())
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.verify
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
class SelfVerificationFailureViewModel : ViewModel() {
private val submitDebugLogRepository: SubmitDebugLogRepository = SubmitDebugLogRepository()
private val internalUiState = MutableStateFlow(VerificationUiState())
val uiState: StateFlow<VerificationUiState> = internalUiState
fun submitLogs() {
viewModelScope.launch {
internalUiState.update { it.copy(showAsProgress = true) }
submitDebugLogRepository.buildAndSubmitLog { result ->
internalUiState.update { it.copy(sendEmail = true, debugLogUrl = result.orNull()) }
}
}
}
}
data class VerificationUiState(
val showAsProgress: Boolean = false,
val sendEmail: Boolean = false,
val debugLogUrl: String? = null
)

View File

@@ -31,15 +31,14 @@ object VerifySafetyNumberRepository {
val aci = recipient.requireAci().libSignalAci
val e164 = recipient.requireE164()
val unidentifiedAccessKey = ProfileKeyUtil.profileKeyOrNull(recipient.profileKey).let { UnidentifiedAccess.deriveAccessKeyFrom(it) }
val monitorMode = if (recipient.isSelf) KeyTransparency.MonitorMode.SELF else KeyTransparency.MonitorMode.OTHER
val firstSearch = recipient.keyTransparencyData == null
val result = if (firstSearch) {
Log.i(TAG, "First search in key transparency")
SignalNetwork.keyTransparency.search(aci, aciIdentityKey, e164, unidentifiedAccessKey, KeyTransparencyStore)
SignalNetwork.keyTransparency.search(aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash = null, KeyTransparencyStore)
} else {
Log.i(TAG, "Monitoring search in key transparency")
SignalNetwork.keyTransparency.monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, KeyTransparencyStore)
SignalNetwork.keyTransparency.monitor(KeyTransparency.MonitorMode.OTHER, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash = null, KeyTransparencyStore)
}
Log.i(TAG, "Key transparency complete, result: $result")