mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Self-check key transparency.
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3864,14 +3864,28 @@
|
||||
<string name="EncryptionVerifiedSheet__body_unavailable">Signal can only automatically verify the encryption in chats where you’re 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 you’re 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user