From afd51395944fd8996f4b2a5e87a626984cd87b52 Mon Sep 17 00:00:00 2001 From: Wolfgang Mathurin Date: Fri, 6 Mar 2026 16:00:32 -0800 Subject: [PATCH] @W-17366971: Clear encryption keys and user info SharedPreferences on logout Addresses ASA security vulnerability (CWE-922) where identifier.xml and current_user_info files persisted after logout, containing encrypted identifiers and user data. Cleanup now occurs when the last authenticated user logs out. --- .../accounts/UserAccountManager.java | 12 +++++++ .../androidsdk/app/SalesforceSDKManager.kt | 5 +++ .../security/SalesforceKeyGenerator.java | 11 ++++++ .../accounts/UserAccountManagerTest.java | 36 +++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java index 7620c16f67..e56d3e24f5 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java @@ -767,4 +767,16 @@ private Bundle buildAuthBundle(UserAccount userAccount) { private String decryptUserData(Account account, String key, String encryptionKey) { return SalesforceSDKManager.decrypt(accountManager.getUserData(account, key), encryptionKey); } + + /** + * Clears the stored current user info from shared preferences. This should be called + * when the last user logs out to ensure no user information remains on the device. + */ + public void clearStoredCurrentUserInfo() { + clearCachedCurrentUser(); + final SharedPreferences sp = context.getSharedPreferences(CURRENT_USER_PREF, + Context.MODE_PRIVATE); + sp.edit().clear().apply(); + SalesforceSDKLogger.d(TAG, "Cleared current user info from shared preferences"); + } } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt index 2749562bea..8f3eb0ce9f 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt @@ -128,6 +128,7 @@ import com.salesforce.androidsdk.rest.NotificationsActionsResponseBody import com.salesforce.androidsdk.rest.NotificationsApiClient import com.salesforce.androidsdk.rest.RestClient import com.salesforce.androidsdk.security.BiometricAuthenticationManager +import com.salesforce.androidsdk.security.SalesforceKeyGenerator import com.salesforce.androidsdk.security.SalesforceKeyGenerator.getEncryptionKey import com.salesforce.androidsdk.security.ScreenLockManager import com.salesforce.androidsdk.ui.AccountSwitcherActivity @@ -790,6 +791,10 @@ open class SalesforceSDKManager protected constructor( (screenLockManager as ScreenLockManager?)?.reset() screenLockManager = null biometricAuthenticationManager = null + + // Clear stored identifiers and user info from shared preferences + SalesforceKeyGenerator.clearAll() + userAccountManager.clearStoredCurrentUserInfo() } } diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java b/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java index 3f54dee890..ad6b5f3613 100644 --- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java +++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/security/SalesforceKeyGenerator.java @@ -224,4 +224,15 @@ private static String getSharedPrefKey(String name) { final String suffix = TextUtils.isEmpty(name) ? "" : name; return String.format(Locale.US, ENCRYPTED_ID_SHARED_PREF_KEY, suffix); } + + /** + * Clears all stored identifiers from shared preferences. This should be called + * when the last user logs out to ensure no encrypted identifiers remain on the device. + */ + public synchronized static void clearAll() { + final SharedPreferences prefs = SalesforceSDKManager.getInstance().getAppContext().getSharedPreferences(SHARED_PREF_FILE, 0); + prefs.edit().clear().apply(); + CACHED_ENCRYPTION_KEYS.clear(); + SalesforceSDKLogger.d(TAG, "Cleared all identifiers from shared preferences"); + } } diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountManagerTest.java b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountManagerTest.java index 9ed86474b3..d8e08d11ef 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountManagerTest.java +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/accounts/UserAccountManagerTest.java @@ -34,6 +34,7 @@ import android.accounts.AccountManager; import android.content.Context; import android.content.IntentFilter; +import android.content.SharedPreferences; import androidx.core.content.ContextCompat; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -42,6 +43,7 @@ import com.salesforce.androidsdk.app.SalesforceSDKManager; import com.salesforce.androidsdk.auth.OAuth2; +import com.salesforce.androidsdk.security.SalesforceKeyGenerator; import com.salesforce.androidsdk.util.LogoutCompleteReceiver; import org.jetbrains.annotations.NotNull; @@ -201,6 +203,40 @@ public void testSignoutBackgroundUser() { Assert.assertEquals(TEST_USERNAME, logoutCompleteReceiver.getLastUserAccountReceived().getUsername()); } + /** + * Test that shared preferences are cleared when the last user logs out. + * This verifies the fix for W-17366971 - ensuring that identifier.xml + * and current_user_info files are deleted on logout. + */ + @Test + public void testSharedPreferencesCleanupOnLastUserLogout() { + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + // Create a user and force creation of identifier.xml by accessing the encryption key + createTestAccountInAccountManager(userAccMgr); + String encryptionKey = SalesforceKeyGenerator.getEncryptionKey("test_key"); + Assert.assertNotNull("Encryption key should be created", encryptionKey); + + // Verify that identifier.xml shared preferences exists with data + SharedPreferences identifierPrefs = context.getSharedPreferences("identifier.xml", 0); + Assert.assertFalse("identifier.xml should have data", identifierPrefs.getAll().isEmpty()); + + // Verify that current_user_info shared preferences exists with data + SharedPreferences currentUserPrefs = context.getSharedPreferences("current_user_info", 0); + Assert.assertFalse("current_user_info should have data", currentUserPrefs.getAll().isEmpty()); + + // Logout the last user + userAccMgr.signoutCurrentUser(null, false, OAuth2.LogoutReason.USER_LOGOUT); + + // Verify that identifier.xml shared preferences is cleared + identifierPrefs = context.getSharedPreferences("identifier.xml", 0); + Assert.assertTrue("identifier.xml should be empty after logout", identifierPrefs.getAll().isEmpty()); + + // Verify that current_user_info shared preferences is cleared + currentUserPrefs = context.getSharedPreferences("current_user_info", 0); + Assert.assertTrue("current_user_info should be empty after logout", currentUserPrefs.getAll().isEmpty()); + } + /** * Removes any existing accounts. */