From 370fca3c8944cbe0369a75e53ec0ee619558371f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 5 May 2026 15:31:35 -0300 Subject: [PATCH] Block screen recording during registration by applying FLAG_SECURE. Co-authored-by: Greyson Parrelli --- .../v2/ui/subscription/EnterKeyScreen.kt | 3 + .../MessageBackupsKeyRecordScreen.kt | 3 + .../components/TemporaryScreenshotSecurity.kt | 55 ++++++++++++++++--- .../registration/ui/RegistrationActivity.kt | 3 - .../ui/restore/EnterBackupKeyScreen.kt | 3 + .../local/EnterLocalBackupKeyScreen.kt | 3 + 6 files changed, 59 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/EnterKeyScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/EnterKeyScreen.kt index d0634e7d54..7ccdb9aef3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/EnterKeyScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/EnterKeyScreen.kt @@ -43,6 +43,7 @@ import org.signal.core.models.AccountEntropyPool import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.horizontalGutters import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity import org.thoughtcrime.securesms.fonts.MonoTypeface import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper @@ -59,6 +60,8 @@ fun EnterKeyScreen( captionContent: @Composable () -> Unit, seeKeyButton: @Composable () -> Unit ) { + TemporaryScreenshotSecurity.bind() + Column( verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index b43f21c051..18be4ac78f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -59,6 +59,7 @@ import org.signal.core.ui.compose.horizontalGutters import org.signal.core.ui.compose.theme.SignalTheme import org.signal.core.util.Util import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState import org.thoughtcrime.securesms.fonts.MonoTypeface @@ -133,6 +134,8 @@ fun MessageBackupsKeyRecordScreen( mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {}), notifyKeyIsSameAsOnDeviceBackupKey: Boolean = false ) { + TemporaryScreenshotSecurity.bind() + val snackbarHostState = remember { SnackbarHostState() } val backupKeyString = remember(backupKey) { backupKey.chunked(4).joinToString(" ") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TemporaryScreenshotSecurity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/TemporaryScreenshotSecurity.kt index 05c9d59fda..2d807d7d47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TemporaryScreenshotSecurity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TemporaryScreenshotSecurity.kt @@ -5,18 +5,41 @@ package org.thoughtcrime.securesms.components +import android.view.Window import android.view.WindowManager import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import org.thoughtcrime.securesms.util.TextSecurePreferences +import java.util.WeakHashMap /** * Applies temporary screenshot security for the given component lifecycle. + * + * Multiple callers can request security on the same window concurrently; the + * flag is only cleared once every caller has released its hold. */ object TemporaryScreenshotSecurity { + private val activeHolds = WeakHashMap() + + @Composable + fun bind() { + val activity = LocalActivity.current as? ComponentActivity ?: return + + DisposableEffect(activity) { + acquire(activity) + + onDispose { + release(activity) + } + } + } + @JvmStatic fun bindToViewLifecycleOwner(fragment: Fragment) { val observer = LifecycleObserver { fragment.requireActivity() } @@ -31,21 +54,37 @@ object TemporaryScreenshotSecurity { activity.lifecycle.addObserver(observer) } + private fun acquire(activity: ComponentActivity) { + val window = activity.window + val previous = activeHolds[window] ?: 0 + activeHolds[window] = previous + 1 + if (previous == 0 && !TextSecurePreferences.isScreenSecurityEnabled(activity)) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + private fun release(activity: ComponentActivity) { + val window = activity.window + val next = ((activeHolds[window] ?: 0) - 1).coerceAtLeast(0) + if (next == 0) { + activeHolds.remove(window) + if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } else { + activeHolds[window] = next + } + } + private class LifecycleObserver( private val activityProvider: () -> ComponentActivity ) : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { - val activity = activityProvider() - if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } + acquire(activityProvider()) } override fun onPause(owner: LifecycleOwner) { - val activity = activityProvider() - if (!TextSecurePreferences.isScreenSecurityEnabled(activity)) { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } + release(activityProvider()) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt index 2f13760c15..e797f1574d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationActivity.kt @@ -12,7 +12,6 @@ import androidx.activity.viewModels import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.navigation.ActivityNavigator -import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.BaseActivity import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R @@ -26,8 +25,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme */ class RegistrationActivity : BaseActivity() { - private val TAG = Log.tag(RegistrationActivity::class.java) - private val dynamicTheme = DynamicNoActionBarTheme() val sharedViewModel: RegistrationViewModel by viewModels() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyScreen.kt index ac7343b197..994aa8646a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyScreen.kt @@ -57,6 +57,7 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.horizontalGutters import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors +import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity import org.thoughtcrime.securesms.fonts.MonoTypeface import org.thoughtcrime.securesms.registration.ui.shared.RegistrationScreen @@ -78,6 +79,8 @@ fun EnterBackupKeyScreen( onSkip: () -> Unit = {}, dialogContent: @Composable () -> Unit ) { + TemporaryScreenshotSecurity.bind() + val coroutineScope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/EnterLocalBackupKeyScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/EnterLocalBackupKeyScreen.kt index df8f793eba..9426b57a63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/EnterLocalBackupKeyScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/EnterLocalBackupKeyScreen.kt @@ -36,6 +36,7 @@ import org.signal.core.ui.compose.CircularProgressWrapper import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity import org.thoughtcrime.securesms.fonts.MonoTypeface import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.ui.restore.AccountEntropyPoolVerification @@ -59,6 +60,8 @@ fun EnterLocalBackupKeyScreen( onRegistrationErrorDismiss: () -> Unit = {}, onBackupKeyHelp: () -> Unit = {} ) { + TemporaryScreenshotSecurity.bind() + val visualTransform = remember { BackupKeyVisualTransformation(chunkSize = 4) } val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() }