From c88ab2b0c0115dcec0d31c33d8abb9a5bc1118c9 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 10:35:33 -0400 Subject: [PATCH 1/5] Guard ApplicationPasswordsStore prefs against Keystore failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Play Console's top WordPress crash (730 users) is `InvalidKeyException` thrown from `AndroidKeystoreAesGcm.encryptInternal` — Tink fails to encrypt/decrypt with the hardware-backed master key after a system update, factory reset of secure hardware, or credential change. `initEncryptedPrefs()` already retries once on init failure, but if the second `createPrefs()` call also throws, the exception propagates out of the `lazy` delegate and from then on every prefs access crashes. Reads and writes via the returned `SharedPreferences` can also fail later when the keystore key becomes inaccessible while the app is still running. - Make `encryptedPreferences` nullable and let the `lazy` block swallow the second-attempt failure, logging it. - Route every public access through a `withEncryptedPrefs(default)` helper that catches `GeneralSecurityException` and other failures, logs them, and returns the supplied default ("" / null / Unit). The Play Console issue has no Sentry id; this is observed only via the Play Developer Reporting API. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ApplicationPasswordsStore.kt | 110 +++++++++++++----- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt index 6c8d75e614e5..5d8019bd0b2f 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt @@ -8,6 +8,7 @@ import okhttp3.Credentials import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.util.AppLog import org.wordpress.android.util.UrlUtils +import java.security.GeneralSecurityException import java.security.KeyStore import javax.inject.Inject import javax.inject.Singleton @@ -27,54 +28,101 @@ class ApplicationPasswordsStore @Inject constructor( there. Do not use directly in WCAndroid app. */ fun getApplicationPasswordAuthHeader(site: SiteModel): String = - Credentials.basic( - username = encryptedPreferences.getString(site.usernamePrefKey, null).orEmpty(), - password = encryptedPreferences.getString(site.passwordPrefKey, null).orEmpty() - ) + withEncryptedPrefs("") { prefs -> + Credentials.basic( + username = prefs.getString(site.usernamePrefKey, null).orEmpty(), + password = prefs.getString(site.passwordPrefKey, null).orEmpty() + ) + } @Inject internal lateinit var configuration: ApplicationPasswordsConfiguration private val applicationName: String get() = configuration.applicationName - private val encryptedPreferences by lazy { - initEncryptedPrefs() + private val encryptedPreferences: SharedPreferences? by lazy { + @Suppress("TooGenericExceptionCaught") + try { + initEncryptedPrefs() + } catch (e: Exception) { + // Both the initial create and the post-delete retry failed; the Keystore-backed + // master key is unrecoverable on this device (Play Console reports this as + // AndroidKeystoreAesGcm.encryptInternal → InvalidKeyException). + AppLog.e( + AppLog.T.MAIN, + "Failed to initialise application-password EncryptedSharedPreferences", + e + ) + null + } } @Synchronized - internal fun getCredentials(site: SiteModel): ApplicationPasswordCredentials? { - val username = encryptedPreferences.getString(site.usernamePrefKey, null) - val password = encryptedPreferences.getString(site.passwordPrefKey, null) - val uuid = encryptedPreferences.getString(site.uuidPrefKey, null) - - return when { - !site.isUsingWpComRestApi && site.username != username -> null - username != null && password != null -> - ApplicationPasswordCredentials( - userName = username, - password = password, - uuid = uuid - ) - else -> null + internal fun getCredentials(site: SiteModel): ApplicationPasswordCredentials? = + withEncryptedPrefs(null) { prefs -> + val username = prefs.getString(site.usernamePrefKey, null) + val password = prefs.getString(site.passwordPrefKey, null) + val uuid = prefs.getString(site.uuidPrefKey, null) + + when { + !site.isUsingWpComRestApi && site.username != username -> null + username != null && password != null -> + ApplicationPasswordCredentials( + userName = username, + password = password, + uuid = uuid + ) + else -> null + } } - } @Synchronized fun saveCredentials(site: SiteModel, credentials: ApplicationPasswordCredentials) { - encryptedPreferences.edit() - .putString(site.usernamePrefKey, credentials.userName) - .putString(site.passwordPrefKey, credentials.password) - .putString(site.uuidPrefKey, credentials.uuid) - .apply() + withEncryptedPrefs(Unit) { prefs -> + prefs.edit() + .putString(site.usernamePrefKey, credentials.userName) + .putString(site.passwordPrefKey, credentials.password) + .putString(site.uuidPrefKey, credentials.uuid) + .apply() + } } @Synchronized fun deleteCredentials(site: SiteModel) { - encryptedPreferences.edit() - .remove(site.usernamePrefKey) - .remove(site.passwordPrefKey) - .remove(site.uuidPrefKey) - .apply() + withEncryptedPrefs(Unit) { prefs -> + prefs.edit() + .remove(site.usernamePrefKey) + .remove(site.passwordPrefKey) + .remove(site.uuidPrefKey) + .apply() + } + } + + // Every read/write to EncryptedSharedPreferences ultimately goes through Tink's + // AndroidKeystoreAesGcm, which can fail with InvalidKeyException long after init + // succeeded (e.g. when the hardware-backed key becomes inaccessible after a system + // update or credential change). Treat any failure as "no stored credentials" so the + // caller can re-authenticate instead of crashing. + @Suppress("TooGenericExceptionCaught") + private inline fun withEncryptedPrefs(default: T, block: (SharedPreferences) -> T): T { + val prefs = encryptedPreferences ?: return default + return try { + block(prefs) + } catch (e: GeneralSecurityException) { + AppLog.e( + AppLog.T.MAIN, + "Keystore failure while accessing application-password preferences", + e + ) + default + } catch (e: Exception) { + AppLog.e( + AppLog.T.MAIN, + "Failed to access application-password preferences", + e + ) + default + } } private fun initEncryptedPrefs(): SharedPreferences { From 25b9dda36cffc606f9e537514f5d3a99bdf35d6d Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 12:46:23 -0400 Subject: [PATCH 2/5] Address review: auth-header default and Unit overload Match prior getApplicationPasswordAuthHeader behavior on Keystore failure by defaulting to Credentials.basic("", "") instead of an empty string, and add a void-returning withEncryptedPrefs overload so the write paths no longer pass Unit explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../applicationpasswords/ApplicationPasswordsStore.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt index 5d8019bd0b2f..472a6cd8da92 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt @@ -28,7 +28,7 @@ class ApplicationPasswordsStore @Inject constructor( there. Do not use directly in WCAndroid app. */ fun getApplicationPasswordAuthHeader(site: SiteModel): String = - withEncryptedPrefs("") { prefs -> + withEncryptedPrefs(Credentials.basic("", "")) { prefs -> Credentials.basic( username = prefs.getString(site.usernamePrefKey, null).orEmpty(), password = prefs.getString(site.passwordPrefKey, null).orEmpty() @@ -78,7 +78,7 @@ class ApplicationPasswordsStore @Inject constructor( @Synchronized fun saveCredentials(site: SiteModel, credentials: ApplicationPasswordCredentials) { - withEncryptedPrefs(Unit) { prefs -> + withEncryptedPrefs { prefs -> prefs.edit() .putString(site.usernamePrefKey, credentials.userName) .putString(site.passwordPrefKey, credentials.password) @@ -89,7 +89,7 @@ class ApplicationPasswordsStore @Inject constructor( @Synchronized fun deleteCredentials(site: SiteModel) { - withEncryptedPrefs(Unit) { prefs -> + withEncryptedPrefs { prefs -> prefs.edit() .remove(site.usernamePrefKey) .remove(site.passwordPrefKey) @@ -103,6 +103,10 @@ class ApplicationPasswordsStore @Inject constructor( // succeeded (e.g. when the hardware-backed key becomes inaccessible after a system // update or credential change). Treat any failure as "no stored credentials" so the // caller can re-authenticate instead of crashing. + private inline fun withEncryptedPrefs(block: (SharedPreferences) -> Unit) { + withEncryptedPrefs(Unit, block) + } + @Suppress("TooGenericExceptionCaught") private inline fun withEncryptedPrefs(default: T, block: (SharedPreferences) -> T): T { val prefs = encryptedPreferences ?: return default From 1ad8ad60e0c1e32d723aab21d39e23222b927d6c Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 16:38:02 -0400 Subject: [PATCH 3/5] Report swallowed Keystore failures and self-heal on recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds onKeystoreError(Throwable) to ApplicationPasswordsListener so the host app can forward Tink/Keystore failures to crash reporting as non-fatals. ApplicationPasswordsStore now notifies the listener from both catch sites (init-time and per-access) and, on a per-access failure, deletes the encrypted prefs file plus the keystore alias and clears the cached SharedPreferences reference so the next access re-initialises with a fresh master key — turning a permanent silent degradation into a one-time re-authentication. WordPress provides WPApplicationPasswordsListener, which routes the report through CrashLogging.sendReportWithTag so spikes remain visible in Sentry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ApplicationPasswordsListenerModule.kt | 16 ++++ .../WPApplicationPasswordsListener.kt | 18 ++++ .../ApplicationPasswordsListener.kt | 5 + .../ApplicationPasswordsStore.kt | 95 +++++++++++++------ 4 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/ApplicationPasswordsListenerModule.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/WPApplicationPasswordsListener.kt diff --git a/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/ApplicationPasswordsListenerModule.kt b/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/ApplicationPasswordsListenerModule.kt new file mode 100644 index 000000000000..c5b6f5454b36 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/ApplicationPasswordsListenerModule.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.fluxc.applicationpasswords + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsListener + +@InstallIn(SingletonComponent::class) +@Module +interface ApplicationPasswordsListenerModule { + @Binds + fun bindApplicationPasswordsListener( + listener: WPApplicationPasswordsListener + ): ApplicationPasswordsListener +} diff --git a/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/WPApplicationPasswordsListener.kt b/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/WPApplicationPasswordsListener.kt new file mode 100644 index 000000000000..361788fd3edb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/fluxc/applicationpasswords/WPApplicationPasswordsListener.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.fluxc.applicationpasswords + +import com.automattic.android.tracks.crashlogging.CrashLogging +import dagger.Lazy +import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsListener +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.crashlogging.sendReportWithTag +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class WPApplicationPasswordsListener @Inject constructor( + private val crashLogging: Lazy +) : ApplicationPasswordsListener { + override fun onKeystoreError(error: Throwable) { + crashLogging.get().sendReportWithTag(error, AppLog.T.MAIN) + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsListener.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsListener.kt index 95caec60d193..6ff65e3e2130 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsListener.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsListener.kt @@ -7,4 +7,9 @@ interface ApplicationPasswordsListener { fun onNewPasswordCreated(isPasswordRegenerated: Boolean) {} fun onPasswordGenerationFailed(networkError: WPAPINetworkError) {} fun onFeatureUnavailable(siteModel: SiteModel, networkError: WPAPINetworkError) {} + + // Hardware-backed Keystore failure (e.g. Tink AndroidKeystoreAesGcm InvalidKeyException) + // when reading or writing the encrypted credentials store. Implementations should treat + // this as a non-fatal so an influx of failures stays visible in crash reporting. + fun onKeystoreError(error: Throwable) {} } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt index 472a6cd8da92..2c1a3db82667 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt @@ -10,12 +10,14 @@ import org.wordpress.android.util.AppLog import org.wordpress.android.util.UrlUtils import java.security.GeneralSecurityException import java.security.KeyStore +import java.util.Optional import javax.inject.Inject import javax.inject.Singleton @Singleton class ApplicationPasswordsStore @Inject constructor( - private val context: Context + private val context: Context, + private val listener: Optional ) { companion object { private const val USERNAME_PREFERENCE_KEY_PREFIX = "username_" @@ -40,10 +42,15 @@ class ApplicationPasswordsStore @Inject constructor( private val applicationName: String get() = configuration.applicationName - private val encryptedPreferences: SharedPreferences? by lazy { + @Volatile + private var encryptedPreferences: SharedPreferences? = null + + @Synchronized + private fun loadEncryptedPreferences(): SharedPreferences? { + encryptedPreferences?.let { return it } @Suppress("TooGenericExceptionCaught") - try { - initEncryptedPrefs() + return try { + initEncryptedPrefs().also { encryptedPreferences = it } } catch (e: Exception) { // Both the initial create and the post-delete retry failed; the Keystore-backed // master key is unrecoverable on this device (Play Console reports this as @@ -53,6 +60,7 @@ class ApplicationPasswordsStore @Inject constructor( "Failed to initialise application-password EncryptedSharedPreferences", e ) + reportKeystoreError(e) null } } @@ -102,14 +110,15 @@ class ApplicationPasswordsStore @Inject constructor( // AndroidKeystoreAesGcm, which can fail with InvalidKeyException long after init // succeeded (e.g. when the hardware-backed key becomes inaccessible after a system // update or credential change). Treat any failure as "no stored credentials" so the - // caller can re-authenticate instead of crashing. + // caller can re-authenticate instead of crashing, and reset the cached prefs so a + // subsequent access re-initialises a fresh keystore-backed file. private inline fun withEncryptedPrefs(block: (SharedPreferences) -> Unit) { withEncryptedPrefs(Unit, block) } @Suppress("TooGenericExceptionCaught") private inline fun withEncryptedPrefs(default: T, block: (SharedPreferences) -> T): T { - val prefs = encryptedPreferences ?: return default + val prefs = loadEncryptedPreferences() ?: return default return try { block(prefs) } catch (e: GeneralSecurityException) { @@ -118,6 +127,8 @@ class ApplicationPasswordsStore @Inject constructor( "Keystore failure while accessing application-password preferences", e ) + reportKeystoreError(e) + invalidateEncryptedPrefs() default } catch (e: Exception) { AppLog.e( @@ -125,35 +136,32 @@ class ApplicationPasswordsStore @Inject constructor( "Failed to access application-password preferences", e ) + reportKeystoreError(e) + invalidateEncryptedPrefs() default } } - private fun initEncryptedPrefs(): SharedPreferences { - val keySpec = MasterKeys.AES256_GCM_SPEC - val filename = "$applicationName-encrypted-prefs" - - fun createPrefs(): SharedPreferences { - val masterKey = MasterKeys.getOrCreate(keySpec) - return EncryptedSharedPreferences.create( - filename, - masterKey, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } + private fun reportKeystoreError(error: Throwable) { + listener.ifPresent { it.onKeystoreError(error) } + } - fun deletePrefs() { - context.deleteSharedPreferences(filename) - with(KeyStore.getInstance("AndroidKeyStore")) { - load(null) - if (containsAlias(keySpec.keystoreAlias)) { - deleteEntry(keySpec.keystoreAlias) - } - } + @Synchronized + @Suppress("TooGenericExceptionCaught", "SwallowedException") + private fun invalidateEncryptedPrefs() { + encryptedPreferences = null + try { + deleteEncryptedPrefsFiles() + } catch (e: Exception) { + AppLog.e( + AppLog.T.MAIN, + "Failed to delete application-password preferences during recovery", + e + ) } + } + private fun initEncryptedPrefs(): SharedPreferences { // The documentation recommends excluding the file from auto backup, but since the file // is defined in an internal library, adding to the backup rules and maintaining them won't // be straightforward. @@ -161,18 +169,43 @@ class ApplicationPasswordsStore @Inject constructor( // We simply delete it and create a new one. @Suppress("TooGenericExceptionCaught", "SwallowedException") return try { - createPrefs() + createEncryptedPrefs() } catch (e: Exception) { // In case we can't decrypt the file after a backup, let's delete it AppLog.d( AppLog.T.MAIN, "Can't decrypt encrypted preferences, delete it and create new one" ) - deletePrefs() - createPrefs() + deleteEncryptedPrefsFiles() + createEncryptedPrefs() + } + } + + private fun createEncryptedPrefs(): SharedPreferences { + val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) + return EncryptedSharedPreferences.create( + encryptedPrefsFilename, + masterKey, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + private fun deleteEncryptedPrefsFiles() { + context.deleteSharedPreferences(encryptedPrefsFilename) + with(KeyStore.getInstance("AndroidKeyStore")) { + load(null) + val alias = MasterKeys.AES256_GCM_SPEC.keystoreAlias + if (containsAlias(alias)) { + deleteEntry(alias) + } } } + private val encryptedPrefsFilename: String + get() = "$applicationName-encrypted-prefs" + private val SiteModel.domainName get() = UrlUtils.removeScheme(url).trim('/') From 7cb49d69618a8f583a7e3ac43e0e22f1f068c657 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 16:42:21 -0400 Subject: [PATCH 4/5] Guard against duplicate Sentry reports on permanent init failure Once initEncryptedPrefs fails after its delete+retry, every subsequent read/write would re-run the same expensive init and emit another Sentry report. Track the permanent-failure state and short-circuit subsequent calls; clear the flag when invalidateEncryptedPrefs() resets the file and keystore alias, so the next attempt gets one clean shot. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ApplicationPasswordsStore.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt index 2c1a3db82667..e0104c3391e3 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt @@ -45,9 +45,18 @@ class ApplicationPasswordsStore @Inject constructor( @Volatile private var encryptedPreferences: SharedPreferences? = null + // Set to true once initEncryptedPrefs has failed even after the delete+retry path; cleared + // by invalidateEncryptedPrefs() since that gives the next init a fresh keystore alias to + // work with. Without this flag, every read/write after a permanent init failure would + // re-run the expensive delete+retry and emit another Sentry report — turning one broken + // device into hundreds of duplicate non-fatals. + @Volatile + private var initPermanentlyFailed: Boolean = false + @Synchronized private fun loadEncryptedPreferences(): SharedPreferences? { encryptedPreferences?.let { return it } + if (initPermanentlyFailed) return null @Suppress("TooGenericExceptionCaught") return try { initEncryptedPrefs().also { encryptedPreferences = it } @@ -55,6 +64,7 @@ class ApplicationPasswordsStore @Inject constructor( // Both the initial create and the post-delete retry failed; the Keystore-backed // master key is unrecoverable on this device (Play Console reports this as // AndroidKeystoreAesGcm.encryptInternal → InvalidKeyException). + initPermanentlyFailed = true AppLog.e( AppLog.T.MAIN, "Failed to initialise application-password EncryptedSharedPreferences", @@ -150,6 +160,9 @@ class ApplicationPasswordsStore @Inject constructor( @Suppress("TooGenericExceptionCaught", "SwallowedException") private fun invalidateEncryptedPrefs() { encryptedPreferences = null + // Files + keystore alias are about to be deleted, so the next init runs against a + // clean slate and deserves another attempt before we declare permanent failure. + initPermanentlyFailed = false try { deleteEncryptedPrefsFiles() } catch (e: Exception) { From 1e8ecb35c9b8441fe157d45d80735d6418232c08 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 17:07:43 -0400 Subject: [PATCH 5/5] Collapse loadEncryptedPreferences to a single return path Detekt's ReturnCount rule rejects three returns; rewrite the cache / permanent-failure / init paths as a when expression so the function exits once. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ApplicationPasswordsStore.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt index e0104c3391e3..2edc4df123c5 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/applicationpasswords/ApplicationPasswordsStore.kt @@ -54,24 +54,27 @@ class ApplicationPasswordsStore @Inject constructor( private var initPermanentlyFailed: Boolean = false @Synchronized + @Suppress("TooGenericExceptionCaught") private fun loadEncryptedPreferences(): SharedPreferences? { - encryptedPreferences?.let { return it } - if (initPermanentlyFailed) return null - @Suppress("TooGenericExceptionCaught") - return try { - initEncryptedPrefs().also { encryptedPreferences = it } - } catch (e: Exception) { - // Both the initial create and the post-delete retry failed; the Keystore-backed - // master key is unrecoverable on this device (Play Console reports this as - // AndroidKeystoreAesGcm.encryptInternal → InvalidKeyException). - initPermanentlyFailed = true - AppLog.e( - AppLog.T.MAIN, - "Failed to initialise application-password EncryptedSharedPreferences", - e - ) - reportKeystoreError(e) - null + val cached = encryptedPreferences + return when { + cached != null -> cached + initPermanentlyFailed -> null + else -> try { + initEncryptedPrefs().also { encryptedPreferences = it } + } catch (e: Exception) { + // Both the initial create and the post-delete retry failed; the Keystore-backed + // master key is unrecoverable on this device (Play Console reports this as + // AndroidKeystoreAesGcm.encryptInternal → InvalidKeyException). + initPermanentlyFailed = true + AppLog.e( + AppLog.T.MAIN, + "Failed to initialise application-password EncryptedSharedPreferences", + e + ) + reportKeystoreError(e) + null + } } }