mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-15 04:30:38 +00:00
android: recognise passphrase fields as passwords for password managers (#7612)
* android: recognise passphrase fields as passwords for password managers Passphrase fields used a hand-rolled VisualTransformation lambda, so Compose never marked them as password fields: the autofill structure carried no hints and no password inputType, and password managers could not identify them. The raw passphrase was also exposed through the CopyText/ACTION_COPY accessibility action. PassphraseField now declares an autofill content type and uses PasswordVisualTransformation, which sets the password semantics Compose derives that flag from. Fields that create a passphrase declare NewPassword. Where no field would carry plain password (first-time setup, hidden profile password) they declare both, as managers do not recognise a form whose every field is newPassword. On the change form the current-passphrase field supplies password, so the new fields declare only newPassword - adding password there leaves the manager unable to tell which field holds the new value and suppresses the save prompt. * android: do not offer to save hidden profile passwords Hidden profile password fields no longer declare an autofill content type. Compose commits the autofill session, which shows the password manager's save prompt, only after the last field with a content type leaves the screen, so these fields never trigger it. A saved entry would reveal that the hidden profile exists. Rewrite the plan to match the code.
This commit is contained in:
+3
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import java.io.File
|
||||
|
||||
@@ -18,3 +19,5 @@ actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
|
||||
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this
|
||||
|
||||
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier
|
||||
|
||||
actual fun newPasswordContentType(): ContentType = ContentType.NewPassword + ContentType.Password
|
||||
|
||||
+3
@@ -3,6 +3,7 @@ package chat.simplex.common.platform
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
@@ -127,3 +128,5 @@ private fun ImageBitmap.blurredBy(radius: Int, drawnWidthDp: Float = CHAT_MEDIA_
|
||||
while (maxOf(image.width, image.height) < BLURRED_MEDIA_WIDTH_DP / 2) image = image.scale(image.width * 2, image.height * 2)
|
||||
return image
|
||||
}
|
||||
|
||||
expect fun newPasswordContentType(): ContentType
|
||||
|
||||
+12
-2
@@ -25,6 +25,9 @@ import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.semantics.contentType
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
@@ -141,7 +144,10 @@ fun DatabaseEncryptionLayout(
|
||||
}
|
||||
}
|
||||
|
||||
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
|
||||
val hasCurrentField = !initialRandomDBPassphrase.value && chatDbEncrypted == true
|
||||
val newKeyContentType =
|
||||
if (hasCurrentField) ContentType.NewPassword else newPasswordContentType()
|
||||
if (hasCurrentField) {
|
||||
PassphraseField(
|
||||
currentKey,
|
||||
generalGetString(MR.strings.current_passphrase),
|
||||
@@ -158,6 +164,7 @@ fun DatabaseEncryptionLayout(
|
||||
showStrength = true,
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
contentType = newKeyContentType,
|
||||
)
|
||||
val onClickUpdate = {
|
||||
// Don't do things concurrently. Shouldn't be here concurrently, just in case
|
||||
@@ -187,6 +194,7 @@ fun DatabaseEncryptionLayout(
|
||||
generalGetString(MR.strings.confirm_new_passphrase),
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||
contentType = newKeyContentType,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (!disabled) onClickUpdate()
|
||||
defaultKeyboardAction(ImeAction.Done)
|
||||
@@ -323,6 +331,7 @@ fun PassphraseField(
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
dependsOn: State<Any?>? = null,
|
||||
requestFocus: Boolean = false,
|
||||
contentType: ContentType? = ContentType.Password,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
@@ -362,6 +371,7 @@ fun PassphraseField(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
)
|
||||
.semantics { if (contentType != null) this.contentType = contentType }
|
||||
.focusRequester(focusRequester),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
@@ -372,7 +382,7 @@ fun PassphraseField(
|
||||
visualTransformation = if (showKey)
|
||||
VisualTransformation.None
|
||||
else
|
||||
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
|
||||
PasswordVisualTransformation('*'),
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
@@ -152,6 +153,7 @@ private fun SetupDatabasePassphraseLayout(
|
||||
showStrength = true,
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
contentType = newPasswordContentType(),
|
||||
)
|
||||
|
||||
PassphraseField(
|
||||
@@ -169,6 +171,7 @@ private fun SetupDatabasePassphraseLayout(
|
||||
},
|
||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
|
||||
contentType = newPasswordContentType(),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
+2
-2
@@ -70,10 +70,10 @@ private fun HiddenProfileLayout(
|
||||
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } }
|
||||
SectionView(stringResource(MR.strings.hidden_profile_password)) {
|
||||
SectionItemViewWithoutMinPadding {
|
||||
PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true)
|
||||
PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true, contentType = null)
|
||||
}
|
||||
SectionItemViewWithoutMinPadding {
|
||||
PassphraseField(confirmHidePassword, stringResource(MR.strings.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword)
|
||||
PassphraseField(confirmHidePassword, stringResource(MR.strings.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword, contentType = null)
|
||||
}
|
||||
SectionItemViewSpaceBetween({ saveProfilePassword(hidePassword.value) }, disabled = saveDisabled, minHeight = TextFieldDefaults.MinHeight) {
|
||||
Text(generalGetString(MR.strings.save_profile_password), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
|
||||
+1
-1
@@ -277,7 +277,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
|
||||
@Composable fun PasswordAndAction(label: StringResource, color: Color = MaterialTheme.colors.primary) {
|
||||
SectionView() {
|
||||
SectionItemViewWithoutMinPadding {
|
||||
PassphraseField(actionPassword, generalGetString(MR.strings.profile_password), isValid = { passwordValid }, showStrength = true)
|
||||
PassphraseField(actionPassword, generalGetString(MR.strings.profile_password), isValid = { passwordValid }, showStrength = true, contentType = null)
|
||||
}
|
||||
SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) {
|
||||
Text(generalGetString(label), color = if (actionEnabled) color else MaterialTheme.colors.secondary)
|
||||
|
||||
+3
@@ -5,6 +5,7 @@ import androidx.compose.foundation.draganddrop.dragAndDropTarget
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.draganddrop.*
|
||||
import androidx.compose.ui.draganddrop.DragData
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
@@ -85,3 +86,5 @@ actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier =
|
||||
this then Modifier
|
||||
.onPointerEvent(PointerEventType.Enter) { action(true) }
|
||||
.onPointerEvent(PointerEventType.Exit) { action(false) }
|
||||
|
||||
actual fun newPasswordContentType(): ContentType = ContentType.NewPassword
|
||||
|
||||
Reference in New Issue
Block a user