Skip to content

Commit 549e563

Browse files
committed
Release v1.8.87 — FLAG_SECURE on encrypted-dictionary passphrase dialog
DictionaryPassphraseDialog hosts both encrypted-export (with confirmation) and encrypted-import passphrase entry for the personal dictionary. While composed, the dialog now: - Sets WindowManager.FLAG_SECURE on the host activity window via a DisposableEffect keyed on the host view; clears the flag on dispose. Without this, screen recordings / external-display mirroring / the system screenshot gesture captured the typed passphrase even with PasswordVisualTransformation in place (the password transformation only masks the rendered glyph, not the surface layer). - Stores `passphrase` / `passphraseConfirmation` in plain `remember` instead of `rememberSaveable`. The latter round-trips through Android's savedInstanceState bundle, which is recoverable via am dumpstate / crash reports and is the wrong storage class for a passphrase. Follow-up #2 from the v1.8.85 audit.
1 parent 8c86966 commit 549e563

3 files changed

Lines changed: 96 additions & 4 deletions

File tree

RELEASE_NOTES_v1.8.87.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Release v1.8.87 — FLAG_SECURE on the encrypted-dictionary passphrase dialog
2+
3+
Date: 2026-05-17
4+
5+
Follow-up #2 from the v1.8.85 audit pass.
6+
7+
## What changed
8+
9+
[app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/dictionary/UserDictionaryScreen.kt](app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/dictionary/UserDictionaryScreen.kt#L742-L780)
10+
— the `DictionaryPassphraseDialog` is the input surface for both
11+
encrypted-export and encrypted-import of the personal dictionary (the
12+
ROADMAP §6 N7.4 encrypted-blob round-trip that shipped in v1.8.54 /
13+
v1.8.65). Two defects:
14+
15+
1. **No `FLAG_SECURE` on the host window.** The host
16+
`FlorisAppActivity` does not set `FLAG_SECURE` for its window globally
17+
(and shouldn't — the rest of Settings is screen-recordable for support /
18+
bug-report screenshots). While the passphrase dialog was up, screen
19+
recordings, external-display mirroring, and the system screenshot
20+
gesture could all capture the typed passphrase. The
21+
`PasswordVisualTransformation` only masks the rendered glyph; the
22+
passphrase characters are still in the surface layer.
23+
2. **Passphrase stored via `rememberSaveable`.** `rememberSaveable` round-
24+
trips state through Android's `savedInstanceState` bundle, which is
25+
recoverable via `am dumpstate`, crash reports, and the platform's
26+
restore-after-process-death path. Passphrase state must not be
27+
serialised.
28+
29+
This release:
30+
31+
- Adds a `DisposableEffect` keyed on the host view that sets
32+
`WindowManager.LayoutParams.FLAG_SECURE` on entry and clears it on
33+
dispose. The flag is set only while the passphrase dialog is composed,
34+
so the rest of Settings remains screen-recordable. Both the export
35+
(with confirmation) and import (without confirmation) flows use the
36+
same dialog, so both are covered.
37+
- Switches `passphrase` and `passphraseConfirmation` from
38+
`rememberSaveable` to plain `remember`. The dialog already re-prompts
39+
on every show (because it's only composed when the visibility flag is
40+
true), so losing in-flight passphrase state across configuration changes
41+
is the correct behaviour, not a regression.
42+
43+
## Files touched
44+
45+
- `app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/dictionary/UserDictionaryScreen.kt`
46+
- `gradle.properties` — versionCode 1887 / versionName 1.8.87
47+
48+
## Verification
49+
50+
```powershell
51+
./gradlew.bat :app:testDebugUnitTest
52+
./gradlew.bat :app:lintDebug
53+
./gradlew.bat :app:assembleDebug
54+
./gradlew.bat :app:installDebug
55+
```
56+
57+
Manual QA:
58+
- Open Settings → User Dictionary → encrypted export. Verify the
59+
passphrase dialog appears. Trigger a system screen recording (or
60+
external-display mirror). Verify the recording shows a black surface
61+
for the dialog area (FLAG_SECURE), not the typed passphrase. Cancel
62+
the dialog and screenshot Settings — that should still work normally
63+
(FLAG_SECURE cleared on dispose).
64+
- Open the same dialog, type a partial passphrase, rotate the device.
65+
Verify the field is empty after rotation (was: it survived rotation
66+
via the savedInstanceState bundle).
67+
- Repeat for the encrypted-import flow.

app/src/main/kotlin/dev/patrickgold/florisboard/app/settings/dictionary/UserDictionaryScreen.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import androidx.compose.material3.OutlinedTextField
4444
import androidx.compose.material3.Text
4545
import androidx.compose.material3.TextButton
4646
import androidx.compose.runtime.Composable
47+
import androidx.compose.runtime.DisposableEffect
4748
import androidx.compose.runtime.LaunchedEffect
4849
import androidx.compose.runtime.getValue
4950
import androidx.compose.runtime.mutableStateOf
@@ -53,10 +54,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
5354
import androidx.compose.runtime.setValue
5455
import androidx.compose.ui.Modifier
5556
import androidx.compose.ui.platform.LocalContext
57+
import androidx.compose.ui.platform.LocalView
5658
import androidx.compose.ui.text.input.ImeAction
5759
import androidx.compose.ui.text.input.KeyboardType
5860
import androidx.compose.ui.text.input.PasswordVisualTransformation
5961
import androidx.compose.ui.unit.dp
62+
import android.app.Activity
63+
import android.view.WindowManager
6064
import dev.patrickgold.florisboard.R
6165
import dev.patrickgold.florisboard.app.LocalNavController
6266
import dev.patrickgold.florisboard.app.settings.theme.DialogProperty
@@ -747,8 +751,29 @@ private fun DictionaryPassphraseDialog(
747751
onDismiss: () -> Unit,
748752
onConfirm: (String) -> Unit,
749753
) {
750-
var passphrase by rememberSaveable { mutableStateOf("") }
751-
var passphraseConfirmation by rememberSaveable { mutableStateOf("") }
754+
// While this dialog is on-screen, mark the host activity window as
755+
// FLAG_SECURE so screen recordings / screenshots / external-display
756+
// mirroring cannot capture the passphrase the user is typing. The
757+
// PasswordVisualTransformation only masks the rendered dot/bullet —
758+
// without FLAG_SECURE the typed characters are still in the surface
759+
// layer that a screen recorder captures. Cleared on dispose.
760+
val view = LocalView.current
761+
DisposableEffect(view) {
762+
val window = (view.context as? Activity)?.window
763+
window?.setFlags(
764+
WindowManager.LayoutParams.FLAG_SECURE,
765+
WindowManager.LayoutParams.FLAG_SECURE,
766+
)
767+
onDispose {
768+
window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
769+
}
770+
}
771+
// Use plain `remember` (not `rememberSaveable`) so the passphrase does
772+
// NOT survive process death / configuration change via the savedInstance
773+
// state bundle. A passphrase in a savedInstanceState bundle is
774+
// recoverable via `am dumpstate` and the like.
775+
var passphrase by remember { mutableStateOf("") }
776+
var passphraseConfirmation by remember { mutableStateOf("") }
752777
val mismatch = requireConfirmation &&
753778
passphraseConfirmation.isNotEmpty() &&
754779
passphrase != passphraseConfirmation

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ projectMinSdk=26
1515
projectTargetSdk=36
1616
projectCompileSdk=36
1717

18-
projectVersionCode=1886
19-
projectVersionName=1.8.86
18+
projectVersionCode=1887
19+
projectVersionName=1.8.87

0 commit comments

Comments
 (0)