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

@@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.BackupRefreshJob;
import org.thoughtcrime.securesms.jobs.BackupSubscriptionCheckJob;
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
import org.thoughtcrime.securesms.jobs.CheckKeyTransparencyJob;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
@@ -261,6 +262,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
checkFreeDiskSpace();
MemoryTracker.start();
BackupSubscriptionCheckJob.enqueueIfAble();
CheckKeyTransparencyJob.enqueueIfNecessary();
AppDependencies.getAuthWebSocket().registerKeepAliveToken(SignalWebSocket.FOREGROUND_KEEPALIVE);
AppDependencies.getUnauthWebSocket().registerKeepAliveToken(SignalWebSocket.FOREGROUND_KEEPALIVE);

View File

@@ -68,6 +68,7 @@ class PhoneNumberPrivacySettingsViewModel : ViewModel() {
private fun setDiscoverableByPhoneNumber(discoverable: Boolean) {
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (discoverable) PhoneNumberDiscoverabilityMode.DISCOVERABLE else PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
SignalDatabase.recipients.clearSelfKeyTransparencyData()
StorageSyncHelper.scheduleSyncForDataChange()
AppDependencies.jobManager.startChain(RefreshAttributesJob()).then(RefreshOwnProfileJob()).enqueue()
refresh()

View File

@@ -161,8 +161,8 @@ import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter;
import org.thoughtcrime.securesms.components.SignalProgressDialog;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.verify.SelfVerificationFailureSheet;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.window.WindowSizeClassExtensionsKt;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
@@ -402,6 +402,12 @@ public class ConversationListFragment extends MainFragment implements Conversati
onSearchQueryUpdated(query);
}
if (SignalStore.settings().getAutomaticVerificationEnabled() &&
SignalStore.misc().getHasKeyTransparencyFailure() &&
!SignalStore.misc().getHasSeenKeyTransparencyFailure()) {
SelfVerificationFailureSheet.show(getParentFragmentManager());
}
RatingManager.showRatingDialogIfNecessary(requireContext());
chatListBackHandler = new ChatListBackHandler(false);

View File

@@ -2234,6 +2234,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.values(NEEDS_PNI_SIGNATURE to 0)
.run()
clearSelfKeyTransparencyData()
SignalDatabase.pendingPniSignatureMessages.deleteAll()
db.setTransactionSuccessful()
@@ -2262,6 +2263,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
if (id == Recipient.self().id) {
clearSelfKeyTransparencyData()
}
if (update(id, contentValuesOf(USERNAME to username))) {
AppDependencies.databaseObserver.notifyRecipientChanged(id)
rotateStorageId(id)
@@ -4056,6 +4061,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.run()
}
fun clearSelfKeyTransparencyData() {
writableDatabase
.update(TABLE_NAME)
.values(KEY_TRANSPARENCY_DATA to null)
.where("$ACI_COLUMN = ?", Recipient.self().requireAci().toString())
.run()
}
/**
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.

View File

@@ -26,9 +26,9 @@ class KeyTransparencyApi(private val unauthWebSocket: SignalWebSocket.Unauthenti
/**
* Uses KT to verify recipient. This is an unauthenticated and should only be called the first time KT is being requested for this recipient.
*/
suspend fun search(aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
suspend fun search(aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String?, unidentifiedAccessKey: ByteArray?, usernameHash: ByteArray?, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
chatConnection.keyTransparencyClient().search(aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
chatConnection.keyTransparencyClient().search(aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, keyTransparencyStore)
.mapWithCancellation(
onSuccess = { RequestResult.Success(Unit) },
onError = { throwable ->
@@ -60,9 +60,9 @@ class KeyTransparencyApi(private val unauthWebSocket: SignalWebSocket.Unauthenti
/**
* Monitors KT to verify recipient. This is an unauthenticated and should only be called following a successful [search].
*/
suspend fun monitor(monitorMode: KeyTransparency.MonitorMode, aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String, unidentifiedAccessKey: ByteArray, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
suspend fun monitor(monitorMode: KeyTransparency.MonitorMode, aci: ServiceId.Aci, aciIdentityKey: IdentityKey, e164: String?, unidentifiedAccessKey: ByteArray?, usernameHash: ByteArray?, keyTransparencyStore: KeyTransparencyStore): RequestResult<Unit, KeyTransparencyError> {
return unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection ->
chatConnection.keyTransparencyClient().monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, null, keyTransparencyStore)
chatConnection.keyTransparencyClient().monitor(monitorMode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, keyTransparencyStore)
.mapWithCancellation(
onSuccess = { RequestResult.Success(Unit) },
onError = { throwable ->

View File

@@ -0,0 +1,208 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.signal.libsignal.keytrans.KeyTransparencyException
import org.signal.libsignal.keytrans.VerificationFailedException
import org.signal.libsignal.net.AppExpiredException
import org.signal.libsignal.net.KeyTransparency
import org.signal.libsignal.net.RequestResult
import org.signal.libsignal.usernames.Username
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.KeyTransparencyStore
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.CoroutineJob
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.CheckKeyTransparencyJobData
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
/**
* Checks verification of our own identifiers using key transparency.
*/
class CheckKeyTransparencyJob private constructor(
private val showFailure: Boolean,
parameters: Parameters
) : CoroutineJob(parameters) {
companion object {
private val TAG = Log.tag(CheckKeyTransparencyJob::class)
const val KEY = "CheckKeyTransparencyJob"
private val TIME_BETWEEN_CHECK = 7.days
@JvmStatic
fun enqueueIfNecessary() {
if (!canRunJob()) {
return
}
val nextCheckIn = SignalStore.misc.lastKeyTransparencyTime.milliseconds + TIME_BETWEEN_CHECK
if (nextCheckIn.inWholeMilliseconds < System.currentTimeMillis()) {
AppDependencies.jobManager.add(
CheckKeyTransparencyJob(
showFailure = false,
parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setInitialDelay(5.minutes.inWholeMilliseconds)
.setGlobalPriority(Parameters.PRIORITY_LOWER)
.setMaxInstancesForFactory(2)
.build()
)
)
}
}
/**
* Following a failure, runs another job that will now show an error if it fails again.
*/
fun enqueueFollowingFailure() {
if (!canRunJob()) {
return
}
AppDependencies.jobManager.add(
CheckKeyTransparencyJob(
showFailure = true,
parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setInitialDelay(1.days.inWholeMilliseconds)
.setGlobalPriority(Parameters.PRIORITY_LOWER)
.build()
)
)
}
private fun canRunJob(): Boolean {
return if (!RemoteConfig.keyTransparency) {
Log.i(TAG, "Remote config is not on. Exiting.")
false
} else if (!SignalStore.account.isRegistered) {
Log.i(TAG, "Account not registered. Exiting.")
false
} else if (!SignalStore.settings.automaticVerificationEnabled) {
Log.i(TAG, "Automatic verification disabled. Exiting.")
false
} else if (SignalStore.account.usernameSyncState != AccountValues.UsernameSyncState.IN_SYNC) {
Log.i(TAG, "Username is in a bad state. Exiting.")
false
} else if (!Recipient.self().hasAci || !Recipient.self().hasE164) {
Log.i(TAG, "Missing an ACI or E164. Exiting.")
false
} else {
true
}
}
}
override suspend fun doRun(): Result {
if (!canRunJob()) {
return Result.failure()
}
SignalStore.misc.lastKeyTransparencyTime = System.currentTimeMillis()
val recipient = SignalDatabase.recipients.getRecord(Recipient.self().id)
val aciIdentityKey = SignalStore.account.aciIdentityKey.publicKey
val aci = recipient.aci!!.libSignalAci
val (e164, unidentifiedAccessKey) = if (SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.DISCOVERABLE) {
Pair(recipient.e164!!, ProfileKeyUtil.profileKeyOrNull(recipient.profileKey).let { UnidentifiedAccess.deriveAccessKeyFrom(it) })
} else {
Pair(null, null)
}
val usernameHash = SignalStore.account.username?.let { Username(it).hash }
val firstSearch = recipient.keyTransparencyData == null
val result = if (firstSearch) {
Log.i(TAG, "First search in key transparency")
SignalNetwork.keyTransparency.search(aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, KeyTransparencyStore)
} else {
Log.i(TAG, "Monitoring search in key transparency")
SignalNetwork.keyTransparency.monitor(KeyTransparency.MonitorMode.SELF, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, KeyTransparencyStore)
}
Log.i(TAG, "Key transparency complete, result: $result")
return when (result) {
is RequestResult.Success -> {
SignalStore.misc.hasKeyTransparencyFailure = false
SignalStore.misc.hasSeenKeyTransparencyFailure = false
Result.success()
}
is RequestResult.NonSuccess -> {
if (result.error.exception is IllegalArgumentException) {
Log.w(TAG, "KT store was corrupted. Restarting and then retrying.")
SignalStore.account.distinguishedHead = null
SignalDatabase.recipients.clearSelfKeyTransparencyData()
Result.retry(defaultBackoff())
} else if (result.error.exception is VerificationFailedException || result.error.exception is KeyTransparencyException) {
if (!showFailure) {
Log.w(TAG, "Verification failure. Enqueuing this job again to run again a day.")
StorageSyncJob.forRemoteChange()
enqueueFollowingFailure()
} else {
Log.w(TAG, "Second verification failure. Showing failure sheet.")
markFailure()
}
Result.failure()
} else if (result.error.exception is AppExpiredException) {
Result.failure()
} else {
Log.w(TAG, "Unknown nonsuccess failure. Showing failure sheet.")
markFailure()
Result.failure()
}
}
is RequestResult.RetryableNetworkError -> {
if (result.retryAfter != null) {
Result.retry(result.retryAfter!!.toMillis())
} else {
Result.retry(defaultBackoff())
}
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "Unknown application failure. Showing failure sheet.")
markFailure()
Result.failure()
}
}
}
/**
* Flags a failure in key transparency. For internal users, always force it to be shown.
* For others, it will only show once and only be cleared on the next successful verification.
*/
private fun markFailure() {
SignalStore.misc.hasKeyTransparencyFailure = true
if (RemoteConfig.internalUser) {
SignalStore.misc.hasSeenKeyTransparencyFailure = false
}
}
override fun serialize(): ByteArray {
return CheckKeyTransparencyJobData(showFailure).encode()
}
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
class Factory : Job.Factory<CheckKeyTransparencyJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): CheckKeyTransparencyJob {
val jobData = CheckKeyTransparencyJobData.ADAPTER.decode(serializedData!!)
return CheckKeyTransparencyJob(jobData.showFailure, parameters)
}
}
}

View File

@@ -154,6 +154,7 @@ public final class JobManagerFactories {
put(CallQualitySurveySubmissionJob.KEY, new CallQualitySurveySubmissionJob.Factory());
put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory());
put(CancelRestoreMediaJob.KEY, new CancelRestoreMediaJob.Factory());
put(CheckKeyTransparencyJob.KEY, new CheckKeyTransparencyJob.Factory());
put(CheckRestoreMediaLeftJob.KEY, new CheckRestoreMediaLeftJob.Factory());
put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory());
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());

View File

@@ -43,6 +43,9 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
private const val NEW_LINKED_DEVICE_CREATED_TIME = "misc.new_linked_device_created_time"
private const val STARTED_QUOTE_THUMBNAIL_MIGRATION = "misc.started_quote_thumbnail_migration"
private const val PREFERRED_MAIN_ACTIVITY_ANCHOR_INDEX = "misc.preferred_main_activity_anchor_index"
private const val LAST_KEY_TRANSPARENCY_TIME = "misc.last_key_transparency_time"
private const val HAS_KEY_TRANSPARENCY_FAILURE = "misc.has_key_transparency_failure"
private const val HAS_SEEN_KEY_TRANSPARENCY_FAILURE = "misc.has_seen_key_transparency_failure"
}
public override fun onFirstEverAppLaunch() {
@@ -291,4 +294,19 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
*/
@get:JvmName("startedQuoteThumbnailMigration")
var startedQuoteThumbnailMigration: Boolean by booleanValue(STARTED_QUOTE_THUMBNAIL_MIGRATION, false)
/**
* The last time we ran key transparency against ourself
*/
var lastKeyTransparencyTime: Long by longValue(LAST_KEY_TRANSPARENCY_TIME, 0)
/**
* Whether you are unable to run key transparency on yourself
*/
var hasKeyTransparencyFailure: Boolean by booleanValue(HAS_KEY_TRANSPARENCY_FAILURE, false)
/**
* Whether you have seen the dialog on key transparency failure
*/
var hasSeenKeyTransparencyFailure: Boolean by booleanValue(HAS_SEEN_KEY_TRANSPARENCY_FAILURE, false)
}

View File

@@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistratio
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.gcm.FcmUtil
import org.thoughtcrime.securesms.jobmanager.runJobBlocking
import org.thoughtcrime.securesms.jobs.CheckKeyTransparencyJob
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
@@ -243,6 +244,9 @@ object RegistrationRepository {
AppDependencies.startNetwork()
PreKeysSyncJob.enqueue()
recipientTable.clearSelfKeyTransparencyData()
CheckKeyTransparencyJob.enqueueIfNecessary()
val jobManager = AppDependencies.jobManager
if (data.linkedDeviceInfo == null) {

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")

View File

@@ -263,4 +263,8 @@ message UnpinJobData {
message CallQualitySurveySubmissionJobData {
org.signal.storageservice.protos.calls.quality.SubmitCallQualitySurveyRequest request = 1;
bool includeDebugLogs = 2;
}
message CheckKeyTransparencyJobData{
bool showFailure = 1;
}

View File

@@ -3864,14 +3864,28 @@
<string name="EncryptionVerifiedSheet__body_unavailable">Signal can only automatically verify the encryption in chats where youre connected to someone via a phone number. If the chat was started with a username or a group in common, verify end-to-end encryption by comparing the numbers on the previous screen or scanning the code on their device.</string>
<!-- Title for auto verification education sheet -->
<string name="VerifyAutomaticallyEducationSheet__title">Signal now auto-verifies end-to-end encryption</string>
<string name="VerifyAutomaticallyEducationSheet__title">Signal can now auto-verify key encryption</string>
<!-- Body for auto verification education sheet -->
<string name="VerifyAutomaticallyEducationSheet__body">When you verify a safety number, Signal will automatically confirm whether the connection is secure using a process called key transparency. You can still verify connections manually using a QR code or number.</string>
<string name="VerifyAutomaticallyEducationSheet__body">For contacts youre connected to by phone number, Signal can automatically confirm whether the connection is secure using a process called key transparency. For added security, you can still verify connections manually using a QR code or number.</string>
<!-- Button to learn more about automatic verification -->
<string name="VerifyAutomaticallyEducationSheet__learn_more">Learn more</string>
<!-- Button to go to verification page -->
<string name="VerifyAutomaticallyEducationSheet__verify">Verify</string>
<!-- Title for bottom sheet when self verification fails -->
<string name="SelfVerificationFailureSheet__title">Automatic Key Verification is currently unavailable for your device. Submit debug log?</string>
<!-- Body for bottom sheet when self verification fails -->
<string name="SelfVerificationFailureSheet__body">Debug logs helps us diagnose and fix the issue, and do not contain identifying information.</string>
<!-- Button to decline submitting logs -->
<string name="SelfVerificationFailureSheet__no_thanks">No thanks</string>
<!-- Button to submit logs -->
<string name="SelfVerificationFailureSheet__submit">Submit</string>
<!-- Email subject line when submitting logs following a verification failure -->
<string name="SelfVerificationFailureSheet__email_subject">AutomaticKeyVerificationFailure</string>
<string name="SelfVerificationFailureSheet__email_filter" translatable="false">AutomaticKeyVerificationFailure</string>
<!-- Link to learn more about debug logs -->
<string name="SelfVerificationFailureSheet__learn_more">Learn more</string>
<!-- verity_scan_fragment -->
<string name="verify_scan_fragment__scan_the_qr_code_on_your_contact">Scan the QR Code on your contact\'s device.</string>