Skip to content

Conversation

sumeruchat
Copy link
Collaborator

@sumeruchat sumeruchat commented Aug 8, 2025

Problem

The Android SDK was crashing on Nexus 5 devices during initialization with:

java.security.KeyStoreException: Entry must be a PrivateKeyEntry or TrustedCertificateEntry; was SecretKeyEntry: algorithm - AES

This occurs because older Android KeyStore implementations (like on Nexus 5/API 23) don't support SecretKeyEntry for AES keys - they only support PrivateKeyEntry and TrustedCertificateEntry.

Related Ticket: MOB-11856

Solution

Minimal fix - added try-catch around IterableDataEncryptor() initialization in the keychain:

try {
    encryptor = IterableDataEncryptor()
    IterableLogger.v(TAG, "SharedPreferences being used with encryption")
} catch (e: Exception) {
    IterableLogger.e(TAG, "Failed to initialize encryption, falling back to plain text", e)
    handleDecryptionError(e)
    return
}

What happens now on Nexus 5:

  1. IterableDataEncryptor() constructor fails with KeyStoreException
  2. Exception is caught in keychain initialization
  3. handleDecryptionError() is called → encryption disabled permanently
  4. App continues working with plain text storage instead of crashing

Changes:

  • 7 lines added to IterableKeychain.kt - wrap encryptor creation in try-catch
  • 52 lines test added - documents and verifies the fix behavior
  • No breaking changes to existing encryption/decryption logic

Testing

  • All existing unit tests pass
  • New test testEncryptorInitializationFailureScenario() validates fallback behavior
  • Verifies plaintext storage works after encryption initialization failure

Manual Test Plan

  1. API 23 Emulator Test:

    • Create Nexus 5 API 23 emulator
    • Install app with SDK
    • ✅ No crash on initialization
    • ✅ App works with plain text storage fallback
  2. Functionality Test:

    • Login/logout flows work normally
    • Data persistence functions correctly
    • Encryption gracefully disabled when unavailable

Resolves MOB-11856

- Wrap keyStore.setEntry() in try-catch to handle SecretKeyEntry not supported
- Add encryptor initialization error handling in keychain with graceful fallback
- Resolves MOB-11856 with minimal changes
- Add encryptor initialization error handling in keychain with graceful fallback
- KeyStoreException bubbles up naturally and gets handled properly
- Resolves MOB-11856 with truly minimal changes
- Test documents expected behavior when IterableDataEncryptor initialization fails
- Verifies graceful fallback to plaintext storage continues to work
- Ensures app functionality is maintained after encryption failure
- Reset IterableDataEncryptor.kt to match master exactly
- Only IterableKeychain.kt and test should have changes for minimal fix
@sumeruchat sumeruchat marked this pull request as ready for review August 8, 2025 15:02
Comment on lines -44 to +51
encryptor = IterableDataEncryptor()
IterableLogger.v(TAG, "SharedPreferences being used with encryption")
try {
encryptor = IterableDataEncryptor()
IterableLogger.v(TAG, "SharedPreferences being used with encryption")
} catch (e: Exception) {
IterableLogger.e(TAG, "Failed to initialize encryption, falling back to plain text", e)
handleDecryptionError(e)
return
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good. It actually prevents crashes which were reproducible

Comment on lines 398 to 401
`when`(mockSharedPrefs.getString(eq("iterable-email"), isNull())).thenReturn(testEmail)
`when`(mockSharedPrefs.getString(eq("iterable-user-id"), isNull())).thenReturn(testUserId)
`when`(mockSharedPrefs.getBoolean(eq("iterable-email_plaintext"), eq(false))).thenReturn(true)
`when`(mockSharedPrefs.getBoolean(eq("iterable-user-id_plaintext"), eq(false))).thenReturn(true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is artificially forcing the behavior it wants to test:
It mocks getString() to return the test values
It mocks getBoolean() to return true for the _plaintext flags
Then it calls the methods and verifies they work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay will fix

Copy link
Member

@Ayyanchira Ayyanchira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. However, test methods might be misleading.

Comment on lines 411 to 412
`when`(mockSharedPrefs.getString(eq("iterable-email"), isNull())).thenReturn(testEmail)
`when`(mockSharedPrefs.getString(eq("iterable-user-id"), isNull())).thenReturn(testUserId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For failure scenario, we should check if iterable-email is actually stored null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ayyanchira I have added the failure scenario now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

- Address reviewer feedback: test now properly simulates null data after encryption failure
- Verify that when encryption fails, existing data returns null (graceful degradation)
- Test validates the actual failure behavior, not just successful plaintext storage
Copy link
Member

@Ayyanchira Ayyanchira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit pick:
One addition to test we can add is to verify iterable-email-plaintext to have the stored value.

@sumeruchat sumeruchat merged commit 912f244 into master Aug 26, 2025
3 checks passed
@sumeruchat sumeruchat deleted the fix/mob-11856-keystore-exception branch August 26, 2025 18:22
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