fix: serialize login submission state

This commit is contained in:
2026-08-24 00:49:42 +08:00
parent 1c5caf5a6b
commit 547146792f
3 changed files with 68 additions and 18 deletions
@@ -71,8 +71,9 @@ class KeystoreCredentialStore(
private fun encode(credentials: LoginCredentials): ByteArray = ByteArrayOutputStream().use { output ->
DataOutputStream(output).use { data ->
data.writeUTF(credentials.username.value)
data.writeUTF(credentials.password.value)
data.writeByte(CREDENTIAL_FORMAT_VERSION)
data.writeByteArray(credentials.username.value.toByteArray(Charsets.UTF_8))
data.writeByteArray(credentials.password.value.toByteArray(Charsets.UTF_8))
}
output.toByteArray()
}
@@ -80,8 +81,11 @@ class KeystoreCredentialStore(
private fun decode(value: ByteArray): LoginCredentials = DataInputStream(
ByteArrayInputStream(value),
).use { data ->
val username = data.readUTF()
val password = data.readUTF()
require(data.readUnsignedByte() == CREDENTIAL_FORMAT_VERSION) {
"Unsupported credential data format"
}
val username = data.readByteArray(MAX_CREDENTIAL_FIELD_BYTES).toString(Charsets.UTF_8)
val password = data.readByteArray(MAX_CREDENTIAL_FIELD_BYTES).toString(Charsets.UTF_8)
require(data.available() == 0) { "Unexpected credential data" }
LoginCredentials(Username(username), Password(password))
}
@@ -98,11 +102,26 @@ class KeystoreCredentialStore(
}
companion object {
private const val CREDENTIAL_FORMAT_VERSION = 1
private const val MAX_CREDENTIAL_FIELD_BYTES = 1_048_576
private fun credentialKey(profileId: ProviderProfileId) =
stringPreferencesKey("profile_${profileId.value}_credential")
}
}
private fun DataOutputStream.writeByteArray(value: ByteArray) {
require(value.size <= 1_048_576) { "Credential field is too large" }
writeInt(value.size)
write(value)
}
private fun DataInputStream.readByteArray(maxSize: Int): ByteArray {
val length = readInt()
require(length in 0..maxSize && length <= available()) { "Invalid credential field length" }
return ByteArray(length).also(::readFully)
}
private fun Throwable.isPermanentCredentialFailure(): Boolean = when (this) {
is AEADBadTagException,
is KeyPermanentlyInvalidatedException,
@@ -33,6 +33,21 @@ class CredentialStoreTest {
assertEquals(secondCredentials, (store.loadCredentials(second) as DomainResult.Success).value)
}
@Test
fun credentialsLargerThanWriteUtfLimitRoundTrip() = runTest {
val file = File.createTempFile("large-credentials", ".preferences_pb").apply { delete() }
val dataStore = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
val store = KeystoreCredentialStore(dataStore, ReversingCipher())
val profile = ProviderProfileId("large")
val credentials = LoginCredentials(
Username("u".repeat(70_000)),
Password("p".repeat(70_000)),
)
assertEquals(DomainResult.Success(Unit), store.saveCredentials(profile, credentials))
assertEquals(credentials, (store.loadCredentials(profile) as DomainResult.Success).value)
}
@Test
fun malformedCredentialIsRemoved() = runTest {
val file = File.createTempFile("malformed-credentials", ".preferences_pb").apply { delete() }