From 456ee211b388b81d051d8ae82053b8cdd39ed4d0 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Mon, 3 Feb 2025 22:03:33 -0600 Subject: [PATCH 1/2] Move sensitive information to encrypted datastore. --- app/build.gradle.kts | 1 + .../java/com/OxGames/Pluvia/CryptoTest.kt | 104 ++++++++++++++++++ .../main/java/com/OxGames/Pluvia/Crypto.kt | 76 +++++++++++++ .../java/com/OxGames/Pluvia/PrefManager.kt | 70 ++++++++---- gradle/libs.versions.toml | 4 +- 5 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 app/src/androidTest/java/com/OxGames/Pluvia/CryptoTest.kt create mode 100644 app/src/main/java/com/OxGames/Pluvia/Crypto.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 940925db..c7416fce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -210,6 +210,7 @@ dependencies { androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.runner) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.test.manifest) testImplementation(libs.junit) diff --git a/app/src/androidTest/java/com/OxGames/Pluvia/CryptoTest.kt b/app/src/androidTest/java/com/OxGames/Pluvia/CryptoTest.kt new file mode 100644 index 00000000..cb8f6fdf --- /dev/null +++ b/app/src/androidTest/java/com/OxGames/Pluvia/CryptoTest.kt @@ -0,0 +1,104 @@ +package com.OxGames.Pluvia + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.security.SecureRandom +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CryptoTest { + + @Test + fun encryptDecrypt_withString_returnsOriginalString() { + val originalString = "Hello, World!" + val originalBytes = originalString.toByteArray() + + val encryptedBytes = Crypto.encrypt(originalBytes) + val decryptedBytes = Crypto.decrypt(encryptedBytes) + val decryptedString = String(decryptedBytes) + + assertNotEquals( + "Encrypted bytes should be different from original", + originalBytes.contentToString(), + encryptedBytes.contentToString(), + ) + assertEquals("Decrypted string should match original", originalString, decryptedString) + } + + @Test + fun encryptDecrypt_withLargeData_succeeds() { + val random = SecureRandom() + val largeData = ByteArray(1024 * 1024) // 1MB + random.nextBytes(largeData) + + val encryptedBytes = Crypto.encrypt(largeData) + val decryptedBytes = Crypto.decrypt(encryptedBytes) + + assertTrue("Decrypted data should match original", largeData.contentEquals(decryptedBytes)) + } + + @Test + fun encryptDecrypt_withEmptyData_throwsException() { + val emptyData = ByteArray(0) + + try { + Crypto.encrypt(emptyData) + fail("Should have thrown IllegalArgumentException") + } catch (_: IllegalArgumentException) { + } + } + + @Test + fun decrypt_withInvalidData_throwsException() { + val invalidData = ByteArray(10) + + try { + Crypto.decrypt(invalidData) + fail("Should have thrown IllegalArgumentException") + } catch (_: IllegalArgumentException) { + } + } + + @Test + fun encrypt_producesRandomOutput() { + val input = "Test".toByteArray() + + val firstEncryption = Crypto.encrypt(input) + val secondEncryption = Crypto.encrypt(input) + + assertNotEquals( + "Multiple encryptions of same data should produce different results", + firstEncryption.contentToString(), + secondEncryption.contentToString(), + ) + } + + @Test + fun encryptDecrypt_withSpecialCharacters_succeeds() { + val specialChars = "!@#$%^&*()_+-=[]{}|;:'\",.<>?/~`" + val originalBytes = specialChars.toByteArray() + + val encryptedBytes = Crypto.encrypt(originalBytes) + val decryptedBytes = Crypto.decrypt(encryptedBytes) + + assertTrue( + "Decrypted special characters should match original", + originalBytes.contentEquals(decryptedBytes), + ) + } + + @Test + fun encryptDecrypt_withMultipleOperations_succeeds() { + val testData = List(10) { "Test data $it".toByteArray() } + + testData.forEach { originalBytes -> + val encryptedBytes = Crypto.encrypt(originalBytes) + val decryptedBytes = Crypto.decrypt(encryptedBytes) + assertTrue( + "Each operation should succeed", + originalBytes.contentEquals(decryptedBytes), + ) + } + } +} diff --git a/app/src/main/java/com/OxGames/Pluvia/Crypto.kt b/app/src/main/java/com/OxGames/Pluvia/Crypto.kt new file mode 100644 index 00000000..595e06bd --- /dev/null +++ b/app/src/main/java/com/OxGames/Pluvia/Crypto.kt @@ -0,0 +1,76 @@ +package com.OxGames.Pluvia + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +/** + * Crypto class that uses the Android KeyStore + * Reference: https://github.com/philipplackner/EncryptedDataStore + */ +object Crypto { + + private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC + private const val KEY_ALIAS = "pluvia_secret" + private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" + + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + // Thread 'Safety' + private fun getCipher(): Cipher = Cipher.getInstance(TRANSFORMATION) + + private fun getKey(): SecretKey { + val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry + return existingKey?.secretKey ?: createKey() + } + + private fun createKey(): SecretKey { + return KeyGenerator + .getInstance(ALGORITHM) + .apply { + val keySpec = KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, + ) + init( + keySpec.setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .setRandomizedEncryptionRequired(true) + .setUserAuthenticationRequired(false) + .setKeySize(256) + .build(), + ) + } + .generateKey() + } + + fun encrypt(bytes: ByteArray): ByteArray { + require(bytes.isNotEmpty()) { + "Input bytes cannot be empty" + } + + val cipher = getCipher() + cipher.init(Cipher.ENCRYPT_MODE, getKey()) + return cipher.iv + cipher.doFinal(bytes) + } + + fun decrypt(bytes: ByteArray): ByteArray { + val cipher = getCipher() + + require(bytes.size > cipher.blockSize) { + "Input bytes too short to contain IV and data. " + + "Minimum length is ${cipher.blockSize + 1}" + } + + val iv = bytes.copyOfRange(0, cipher.blockSize) + val data = bytes.copyOfRange(cipher.blockSize, bytes.size) + cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) + return cipher.doFinal(data) + } +} diff --git a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt index c207faa3..25f8a459 100644 --- a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt +++ b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.byteArrayPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.intPreferencesKey @@ -21,9 +22,9 @@ import com.winlator.container.Container import com.winlator.core.DefaultVersion import `in`.dragonbra.javasteam.enums.EPersonaState import java.util.EnumSet -import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -44,12 +45,30 @@ object PrefManager { }, ) - private val scope = CoroutineScope(Dispatchers.IO + CoroutineName("PrefManager")) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private lateinit var dataStore: DataStore fun init(context: Context) { dataStore = context.datastore + + // Note: Should remove after a few release versions. we've moved to encrypted values. + val oldAccessToken = stringPreferencesKey("access_token") + val oldRefreshToken = stringPreferencesKey("refresh_token") + getPref(oldAccessToken, "").let { + if (it.isNotEmpty()) { + Timber.i("Converting old access token to encrypted") + accessToken = it + removePref(oldAccessToken) + } + } + getPref(oldRefreshToken, "").let { + if (it.isNotEmpty()) { + Timber.i("Converting old refresh token to encrypted") + refreshToken = it + removePref(oldRefreshToken) + } + } } fun clearPreferences() { @@ -76,11 +95,11 @@ object PrefManager { } } - // private fun removePref(key: Preferences.Key) { - // scope.launch { - // dataStore.edit { pref -> pref.remove(key) } - // } - // } + private fun removePref(key: Preferences.Key) { + scope.launch { + dataStore.edit { pref -> pref.remove(key) } + } + } /* Container Default Settings */ private val SCREEN_SIZE = stringPreferencesKey("screen_size") @@ -281,18 +300,36 @@ object PrefManager { setPref(APP_STAGING_PATH, value) } - private val ACCESS_TOKEN = stringPreferencesKey("access_token") + private val ACCESS_TOKEN_ENC = byteArrayPreferencesKey("access_token_enc") var accessToken: String - get() = getPref(ACCESS_TOKEN, "") + get() { + val encryptedBytes = getPref(ACCESS_TOKEN_ENC, ByteArray(0)) + return if (encryptedBytes.isEmpty()) { + "" + } else { + val bytes = Crypto.decrypt(encryptedBytes) + String(bytes) + } + } set(value) { - setPref(ACCESS_TOKEN, value) + val bytes = Crypto.encrypt(value.toByteArray()) + setPref(ACCESS_TOKEN_ENC, bytes) } - private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") + private val REFRESH_TOKEN_ENC = byteArrayPreferencesKey("refresh_token_enc") var refreshToken: String - get() = getPref(REFRESH_TOKEN, "") + get() { + val encryptedBytes = getPref(REFRESH_TOKEN_ENC, ByteArray(0)) + return if (encryptedBytes.isEmpty()) { + "" + } else { + val bytes = Crypto.decrypt(encryptedBytes) + String(bytes) + } + } set(value) { - setPref(REFRESH_TOKEN, value) + val bytes = Crypto.encrypt(value.toByteArray()) + setPref(REFRESH_TOKEN_ENC, bytes) } // Special: Because null value. @@ -305,13 +342,6 @@ object PrefManager { } } - private val REMEMBER_PASSWORD = booleanPreferencesKey("remember_password") - var rememberPassword: Boolean - get() = getPref(REMEMBER_PASSWORD, false) - set(value) { - setPref(REMEMBER_PASSWORD, value) - } - private val PASSWORD = stringPreferencesKey("password") var password: String get() = getPref(PASSWORD, "") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a4bcbd9..15b3ef4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ materialKolor = "2.0.0" # https://mvnrepository.com/artifact/com.materialkolor/m navigation-compose = "2.8.5" # https://mvnrepository.com/artifact/androidx.navigation/navigation-compose protobuf = "4.29.3" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java room-runtime = "2.6.1" # https://mvnrepository.com/artifact/androidx.room/room-runtime +runner = "1.6.2" # https://mvnrepository.com/artifact/androidx.test/runner settings = "2.10.0" # https://github.com/alorma/Compose-Settings/releases spongycastle = "1.58.0.0" # https://mvnrepository.com/artifact/com.madgag.spongycastle/prov steamkit = "1.6.0-SNAPSHOT" # https://mvnrepository.com/artifact/in.dragonbra/javasteam @@ -49,7 +50,7 @@ androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "ro androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room-runtime" } # TODO: Remove 'version' once 1.8.0 is rolled into stable BOM, see https://developer.android.com/jetpack/androidx/releases/compose-ui # This fixes `Placement happened before lookahead` crash when animating lists items. -androidx-ui = { group = "androidx.compose.ui", name = "ui", version = "1.8.0-alpha08" } +androidx-ui = { group = "androidx.compose.ui", name = "ui", version = "1.8.0-beta01" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } @@ -76,6 +77,7 @@ zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } # Testing androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } junit = { group = "junit", name = "junit", version.ref = "junit" } From cebd57fad7770777ff24a8f66a40d69a3c915d13 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Mon, 3 Feb 2025 23:55:22 -0600 Subject: [PATCH 2/2] Remove the use to save password to preferences. Renamed 'Remember me' to "Remember session" --- .../java/com/OxGames/Pluvia/PrefManager.kt | 10 ++--- .../OxGames/Pluvia/service/SteamService.kt | 38 ++++++++----------- .../OxGames/Pluvia/ui/data/UserLoginState.kt | 4 +- .../Pluvia/ui/model/UserLoginViewModel.kt | 6 +-- .../Pluvia/ui/screen/login/UserLoginScreen.kt | 22 +++++------ 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt index 25f8a459..9b36db12 100644 --- a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt +++ b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt @@ -53,6 +53,9 @@ object PrefManager { dataStore = context.datastore // Note: Should remove after a few release versions. we've moved to encrypted values. + val oldPassword = stringPreferencesKey("password") + removePref(oldPassword) + val oldAccessToken = stringPreferencesKey("access_token") val oldRefreshToken = stringPreferencesKey("refresh_token") getPref(oldAccessToken, "").let { @@ -342,13 +345,6 @@ object PrefManager { } } - private val PASSWORD = stringPreferencesKey("password") - var password: String - get() = getPref(PASSWORD, "") - set(value) { - setPref(PASSWORD, value) - } - /** * Get or Set the last known Persona State. See [EPersonaState] */ diff --git a/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt b/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt index dc7fccd1..0cd3a252 100644 --- a/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt +++ b/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt @@ -761,7 +761,7 @@ class SteamService : Service(), IChallengeUrlChanged { accessToken: String? = null, refreshToken: String? = null, password: String? = null, - shouldRememberPassword: Boolean = false, + rememberSession: Boolean = false, twoFactorAuth: String? = null, emailAuth: String? = null, clientId: Long? = null, @@ -771,30 +771,25 @@ class SteamService : Service(), IChallengeUrlChanged { // Sensitive info, only print in DEBUG build. if (BuildConfig.DEBUG) { Timber.d( - "Login Information\n\tUsername: " + - "$username\n\tAccessToken: " + - "$accessToken\n\tRefreshToken: " + - "$refreshToken\n\tPassword: " + - "$password\n\tShouldRememberPass: " + - "$shouldRememberPassword\n\tTwoFactorAuth: " + - "$twoFactorAuth\n\tEmailAuth: $emailAuth", + "Login Information\n\t" + + "Username: $username\n\t" + + "AccessToken: $accessToken\n\t" + + "RefreshToken:$refreshToken\n\t" + + "Password: $password\n\t" + + "Remember Session: $rememberSession\n\t" + + "TwoFactorAuth: $twoFactorAuth\n\t" + + "EmailAuth: $emailAuth", ) } PrefManager.username = username - if ((password != null && shouldRememberPassword) || refreshToken != null) { - if (password != null) { - PrefManager.password = password - } - + if ((password != null && rememberSession) || refreshToken != null) { if (accessToken != null) { - PrefManager.password = "" PrefManager.accessToken = accessToken } if (refreshToken != null) { - PrefManager.password = "" PrefManager.refreshToken = refreshToken } @@ -819,7 +814,7 @@ class SteamService : Service(), IChallengeUrlChanged { } else { null }, - shouldRememberPassword = shouldRememberPassword, + shouldRememberPassword = rememberSession, twoFactorCode = twoFactorAuth, authCode = emailAuth, accessToken = refreshToken, @@ -836,7 +831,7 @@ class SteamService : Service(), IChallengeUrlChanged { suspend fun startLoginWithCredentials( username: String, password: String, - shouldRememberPassword: Boolean, + rememberSession: Boolean, authenticator: IAuthenticator, ) = withContext(Dispatchers.IO) { try { @@ -846,7 +841,7 @@ class SteamService : Service(), IChallengeUrlChanged { val authDetails = AuthSessionDetails().apply { this.username = username.trim() this.password = password.trim() - this.persistentSession = shouldRememberPassword + this.persistentSession = rememberSession this.authenticator = authenticator this.deviceFriendlyName = SteamUtils.getMachineName(instance!!) } @@ -866,7 +861,7 @@ class SteamService : Service(), IChallengeUrlChanged { username = pollResult.accountName, accessToken = pollResult.accessToken, refreshToken = pollResult.refreshToken, - shouldRememberPassword = shouldRememberPassword, + rememberSession = rememberSession, ) } ?: run { Timber.e("Could not logon: Failed to connect to Steam") @@ -1285,14 +1280,13 @@ class SteamService : Service(), IChallengeUrlChanged { var isAutoLoggingIn = false - if (PrefManager.username.isNotEmpty() && (PrefManager.refreshToken.isNotEmpty() || PrefManager.password.isNotEmpty())) { + if (PrefManager.username.isNotEmpty() && PrefManager.refreshToken.isNotEmpty()) { isAutoLoggingIn = true login( username = PrefManager.username, refreshToken = PrefManager.refreshToken, - password = PrefManager.password.ifEmpty { null }, - shouldRememberPassword = PrefManager.password.isNotEmpty(), + rememberSession = true, ) } diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/data/UserLoginState.kt b/app/src/main/java/com/OxGames/Pluvia/ui/data/UserLoginState.kt index dedcf8d6..a3ad51ee 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/data/UserLoginState.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/data/UserLoginState.kt @@ -6,7 +6,7 @@ import com.OxGames.Pluvia.enums.LoginScreen data class UserLoginState( val username: String = "", val password: String = "", - val rememberPass: Boolean = false, + val rememberSession: Boolean = false, val twoFactorCode: String = "", val isSteamConnected: Boolean = false, @@ -26,7 +26,7 @@ data class UserLoginState( return "UserLoginState(" + "username='$username', " + "password='$password', " + - "rememberPass=$rememberPass, " + + "rememberSession=$rememberSession, " + "twoFactorCode='$twoFactorCode', " + "isSteamConnected=$isSteamConnected, " + "isLoggingIn=$isLoggingIn, " + diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/model/UserLoginViewModel.kt b/app/src/main/java/com/OxGames/Pluvia/ui/model/UserLoginViewModel.kt index 7f46c2b5..805a434f 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/model/UserLoginViewModel.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/model/UserLoginViewModel.kt @@ -228,7 +228,7 @@ class UserLoginViewModel : ViewModel() { SteamService.startLoginWithCredentials( username = username, password = password, - shouldRememberPassword = rememberPass, + rememberSession = rememberSession, authenticator = authenticator, ) } @@ -273,9 +273,9 @@ class UserLoginViewModel : ViewModel() { } } - fun setRememberPass(rememberPass: Boolean) { + fun setRememberSession(rememberPass: Boolean) { _loginState.update { currentState -> - currentState.copy(rememberPass = rememberPass) + currentState.copy(rememberSession = rememberPass) } } diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/login/UserLoginScreen.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/login/UserLoginScreen.kt index d3562283..f18c929d 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/login/UserLoginScreen.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/login/UserLoginScreen.kt @@ -89,7 +89,7 @@ fun UserLoginScreen( onUsername = viewModel::setUsername, onPassword = viewModel::setPassword, onShowLoginScreen = viewModel::onShowLoginScreen, - onRememberPassword = viewModel::setRememberPass, + onRememberSession = viewModel::setRememberSession, onCredentialLogin = viewModel::onCredentialLogin, onTwoFactorLogin = viewModel::submit, onQrRetry = viewModel::onQrRetry, @@ -105,7 +105,7 @@ private fun UserLoginScreenContent( onUsername: (String) -> Unit, onPassword: (String) -> Unit, onShowLoginScreen: (LoginScreen) -> Unit, - onRememberPassword: (Boolean) -> Unit, + onRememberSession: (Boolean) -> Unit, onCredentialLogin: () -> Unit, onTwoFactorLogin: () -> Unit, onQrRetry: () -> Unit, @@ -191,8 +191,8 @@ private fun UserLoginScreenContent( onUsername = onUsername, password = userLoginState.password, onPassword = onPassword, - rememberPassword = userLoginState.rememberPass, - onRememberPassword = onRememberPassword, + rememberSession = userLoginState.rememberSession, + onRememberSession = onRememberSession, onLoginBtnClick = onCredentialLogin, ) } @@ -254,8 +254,8 @@ private fun UsernamePassword( onUsername: (String) -> Unit, password: String, onPassword: (String) -> Unit, - rememberPassword: Boolean, - onRememberPassword: (Boolean) -> Unit, + rememberSession: Boolean, + onRememberSession: (Boolean) -> Unit, onLoginBtnClick: () -> Unit, ) { var passwordVisible by remember { mutableStateOf(false) } @@ -297,12 +297,12 @@ private fun UsernamePassword( Row(horizontalArrangement = Arrangement.SpaceBetween) { Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( - checked = rememberPassword, - onCheckedChange = onRememberPassword, + checked = rememberSession, + onCheckedChange = onRememberSession, ) - Text(text = "Remember me") + Text(text = "Remember session") } - Spacer(modifier = Modifier.width(32.dp)) + Spacer(modifier = Modifier.width(24.dp)) ElevatedButton( onClick = onLoginBtnClick, enabled = username.isNotEmpty() && password.isNotEmpty() && isSteamConnected, @@ -335,7 +335,7 @@ private fun Preview_UserLoginScreen( userLoginState = state, onUsername = { }, onPassword = { }, - onRememberPassword = { }, + onRememberSession = { }, onCredentialLogin = { }, onTwoFactorLogin = { }, onQrRetry = { },