Skip to content

@W-17366971: Clear encryption keys and user info SharedPreferences on logout#2848

Merged
wmathurin merged 1 commit intoforcedotcom:devfrom
wmathurin:dev
Mar 7, 2026
Merged

@W-17366971: Clear encryption keys and user info SharedPreferences on logout#2848
wmathurin merged 1 commit intoforcedotcom:devfrom
wmathurin:dev

Conversation

@wmathurin
Copy link
Contributor

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.

… 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.
@github-actions
Copy link

github-actions bot commented Mar 7, 2026

1 Warning
⚠️ libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManager.java#L109 - Do not place Android context classes in static fields (static reference to UserAccountManager which has field context pointing to Context); this is a memory leak

Generated by 🚫 Danger

@wmathurin
Copy link
Contributor Author

Analysis: identifier.xml Usage in Salesforce Mobile SDK for Android

Overview

This document analyzes how the content of identifier.xml SharedPreferences file is used throughout the Salesforce Mobile SDK for Android codebase, in the context of fixing W-17366971 (ASA security vulnerability).

What is identifier.xml?

The identifier.xml file is a SharedPreferences file created by SalesforceKeyGenerator.java that stores encrypted unique identifiers used as the basis for deriving encryption keys throughout the SDK.

File Location

  • Path: /data/data/<package_name>/shared_prefs/identifier.xml.xml
  • Created by: SalesforceKeyGenerator.java (line 59: SHARED_PREF_FILE = "identifier.xml")

Contents

The file contains entries with keys in the format: encrypted_<name> where the values are:

  • RSA-encrypted unique identifiers (AES-256 or UUID-based)
  • Encrypted using a public key from Android KeyStore (alias: MSDK_KEYPAIR_ALIAS)
  • Decrypted using a private key from Android KeyStore

Example from ASA report:

<string name="encrypted_id_6cgs4f">xrKMKKseGRA5sA5y1hsPO+1fIJFrX3RwYdjVXu+KwYEUo1f...</string>

How identifier.xml is Used

1. Master Encryption Key for User Account Data

Location: SalesforceSDKManager.kt (line 1898)

val encryptionKey: String
    get() = getEncryptionKey(INTERNAL_ENTROPY)  // INTERNAL_ENTROPY = "6cgs4f"

Purpose: Derives the master encryption key used to encrypt/decrypt:

  • OAuth tokens (access tokens, refresh tokens)
  • User credentials in AccountManager
  • Login URLs, instance URLs
  • User IDs, Org IDs
  • All sensitive user account data stored in Android's AccountManager

Flow:

  1. getEncryptionKey("6cgs4f") is called
  2. Retrieves/generates unique ID from identifier.xml (key: encrypted_id_6cgs4f)
  3. Hashes with SHA-256 to create a 256-bit encryption key
  4. Used by SalesforceSDKManager.encrypt() and decrypt() methods throughout the SDK

Impact: If this key persists after logout, it could theoretically be used to decrypt cached user data if any remnants exist.


2. SmartStore Database Encryption

Location: SmartStoreSDKManager.java (lines 191, 242, 539, 640)

SQLiteOpenHelper dbOpenHelper = DBOpenHelper.getOpenHelper(getEncryptionKey(), ...)
KeyValueEncryptedFileStore kvStore = new KeyValueEncryptedFileStore(..., getEncryptionKey())

Purpose: Encrypts SmartStore databases using SQLCipher:

  • User-specific SmartStore databases (one per user/community)
  • Global SmartStore databases (shared across users)
  • Key-Value encrypted file stores (for metadata and cache)

How it works:

  • The encryption key from identifier.xml is passed to SQLCipher as the database password
  • SQLCipher uses it to encrypt/decrypt database pages using AES-256
  • Each SmartStore file is encrypted with this key

Important: SmartStore databases are properly deleted on logout via DBOpenHelper.deleteDatabase() and ManagedFilesHelper.deleteFile(), so the data itself is removed. However, if the encryption key remains in identifier.xml, a theoretical attack vector exists if database files aren't fully deleted due to a bug.


3. Analytics Data Encryption

Location: SalesforceAnalyticsManager.java (line 449)

analyticsManager = new AnalyticsManager(filenameSuffix, sdkManager.getAppContext(),
    SalesforceSDKManager.getEncryptionKey(), deviceAppAttributes);

Purpose: Encrypts analytics event data stored locally before upload:

  • User interaction events
  • API call metrics
  • Error logs
  • Performance data

Data stored: Analytics events are stored in encrypted files on disk until they can be uploaded to Salesforce servers.


4. Push Notification Decryption

Location: PushNotificationDecryptor.java (line 126)

final String name = SalesforceKeyGenerator.getUniqueId(PushService.PUSH_NOTIFICATION_KEY_NAME);
final String sanitizedName = name.replaceAll("[^A-Za-z0-9]", "");
rsaPrivateKey = KeyStoreWrapper.getInstance().getRSAPrivateKey(sanitizedName);

Purpose: Retrieves a unique identifier to look up an RSA private key used to decrypt push notifications:

  • Uses identifier from identifier.xml as an alias to find the right RSA keypair
  • Decrypts the symmetric key sent with encrypted push notifications
  • Uses the symmetric key to decrypt the actual push payload

Why it matters: The unique ID acts as a stable identifier for the RSA keypair across app restarts.


5. OAuth2 PKCE Code Verifier/Challenge

Locations:

  • NativeLoginManager.kt (lines 75-76)
  • SPManager.kt (lines 55-56)
val codeVerifier: String = SalesforceKeyGenerator.getRandom128ByteKey()
val codeChallenge: String = SalesforceKeyGenerator.getSHA256Hash(codeVerifier)

Purpose: Generates PKCE (Proof Key for Code Exchange) parameters for OAuth2 authentication:

  • Creates random 128-byte code verifier
  • Hashes it to create code challenge
  • Used in OAuth2 authorization code flow

Note: These use getRandom128ByteKey() which generates new random keys each time (not derived from identifier.xml), so they're not affected by the cleanup.


Security Implications of Not Clearing identifier.xml

High Risk:

  1. Master Encryption Key Exposure

    • The encrypted identifier for "6cgs4f" remains on disk
    • Could be decrypted by malware with device access if Android KeyStore is compromised
    • Allows decryption of any cached encrypted data that wasn't properly cleaned up
  2. Cross-Session Data Correlation

    • If a user logs out and logs in again, the same encryption key is reused
    • Allows correlation of data across login sessions
    • Violates user privacy expectations after logout

Medium Risk:

  1. SmartStore Re-encryption Attack

    • If SmartStore databases aren't fully deleted (file system bug, crash during cleanup, etc.)
    • An attacker could use the persisted encryption key to decrypt old database files
  2. Analytics Data Persistence

    • If analytics files aren't fully flushed/deleted on logout
    • The encryption key could decrypt residual analytics data

Lower Risk (but still important):

  1. Push Notification Key Lookup
    • The push notification RSA keypair alias remains
    • Less sensitive since the RSA private key is in Android KeyStore, but violates defense-in-depth

Why Our Fix is Appropriate

What We're Clearing:

  1. identifier.xml - All encrypted unique identifiers
  2. current_user_info - User ID and Org ID information
  3. Cached encryption keys - In-memory cache in CACHED_ENCRYPTION_KEYS

When We Clear:

  • Only when the last user logs out (users.size <= 1)
  • Consistent with other SDK cleanup (ScreenLockManager, BiometricAuthenticationManager)

Defense in Depth:

Even though the SDK already:

  • Deletes SmartStore databases on logout
  • Clears analytics data via SalesforceAnalyticsManager.reset()
  • Clears REST client caches
  • Removes accounts from AccountManager

...our fix provides an additional security layer by ensuring the encryption keys themselves are removed, making it impossible to decrypt any residual data that might remain due to:

  • File system race conditions
  • Incomplete cleanup due to crashes
  • Bugs in other cleanup code
  • Future features that might cache encrypted data

ASA Vulnerability Details (W-17366971)

Category: CWE-922: Insecure Storage of Sensitive Information
CVSS Score: 3.5 (Low) - CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
Risk: Low (Likelihood: 2, Impact: 2)

Threat: Physical access attacker could retrieve encrypted identifiers and potentially:

  1. Decrypt them if Android KeyStore is compromised (requires root or device exploit)
  2. Correlate user sessions across logins
  3. Access residual encrypted data if cleanup is incomplete

Mitigation: Clear all encryption keys and identifiers when the last user logs out.


Verification

Our test case testSharedPreferencesCleanupOnLastUserLogout() verifies:

  1. identifier.xml is created when encryption keys are used
  2. current_user_info is created when users log in
  3. Both files are completely empty after the last user logs out
  4. Cleanup only happens on last logout (not when switching users)

Conclusion

The content of identifier.xml is critical to the security of all encrypted data in the Salesforce Mobile SDK:

  • It's the root of the encryption key hierarchy
  • Used to encrypt user credentials, tokens, databases, analytics, and more
  • Must be cleared on logout to ensure no encrypted data can be recovered
  • Our fix addresses the ASA vulnerability by ensuring complete cleanup

The fix is necessary, safe, and follows security best practices for mobile applications.

@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.87%. Comparing base (115372e) to head (afd5139).
⚠️ Report is 3 commits behind head on dev.

Additional details and impacted files
@@             Coverage Diff              @@
##                dev    #2848      +/-   ##
============================================
+ Coverage     64.56%   64.87%   +0.31%     
- Complexity     2925     2964      +39     
============================================
  Files           222      222              
  Lines         17338    17380      +42     
  Branches       2474     2477       +3     
============================================
+ Hits          11195    11276      +81     
+ Misses         4936     4905      -31     
+ Partials       1207     1199       -8     
Components Coverage Δ
Analytics 47.92% <ø> (ø)
SalesforceSDK 59.67% <100.00%> (+0.60%) ⬆️
Hybrid 59.05% <ø> (ø)
SmartStore 78.20% <ø> (ø)
MobileSync 81.68% <ø> (ø)
React 52.36% <ø> (ø)
Files with missing lines Coverage Δ
...sforce/androidsdk/accounts/UserAccountManager.java 73.88% <100.00%> (+0.09%) ⬆️
.../salesforce/androidsdk/app/SalesforceSDKManager.kt 59.43% <100.00%> (+0.14%) ⬆️
...ce/androidsdk/security/SalesforceKeyGenerator.java 84.41% <100.00%> (+1.08%) ⬆️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@wmathurin wmathurin merged commit 5f1304b into forcedotcom:dev Mar 7, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants