Skip to content

Commit 9140d3d

Browse files
authored
MOB-10857 fix encryption crash (#884)
1 parent 53c8b98 commit 9140d3d

File tree

2 files changed

+65
-5
lines changed

2 files changed

+65
-5
lines changed

iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt

+22-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class IterableKeychain {
99
const val KEY_EMAIL = "iterable-email"
1010
const val KEY_USER_ID = "iterable-user-id"
1111
const val KEY_AUTH_TOKEN = "iterable-auth-token"
12+
private const val PLAINTEXT_SUFFIX = "_plaintext"
1213
}
1314

1415
private var sharedPrefs: SharedPreferences
@@ -74,6 +75,11 @@ class IterableKeychain {
7475
}
7576

7677
private fun secureGet(key: String): String? {
78+
// First check if it's stored in plaintext
79+
if (sharedPrefs.getBoolean(key + PLAINTEXT_SUFFIX, false)) {
80+
return sharedPrefs.getString(key, null)
81+
}
82+
7783
return try {
7884
sharedPrefs.getString(key, null)?.let { encryptor.decrypt(it) }
7985
} catch (e: Exception) {
@@ -83,9 +89,22 @@ class IterableKeychain {
8389
}
8490

8591
private fun secureSave(key: String, value: String?) {
86-
sharedPrefs.edit()
87-
.putString(key, value?.let { encryptor.encrypt(it) })
88-
.apply()
92+
val editor = sharedPrefs.edit()
93+
if (value == null) {
94+
editor.remove(key).remove(key + PLAINTEXT_SUFFIX).apply()
95+
return
96+
}
97+
98+
try {
99+
editor.putString(key, encryptor.encrypt(value))
100+
.remove(key + PLAINTEXT_SUFFIX)
101+
.apply()
102+
} catch (e: Exception) {
103+
handleDecryptionError(e)
104+
editor.putString(key, value)
105+
.putBoolean(key + PLAINTEXT_SUFFIX, true)
106+
.apply()
107+
}
89108
}
90109

91110
fun getEmail() = secureGet(KEY_EMAIL)

iterableapi/src/test/java/com/iterable/iterableapi/IterableKeychainTest.kt

+43-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import org.mockito.MockedStatic
2323
import org.mockito.Mockito.mockStatic
2424
import org.mockito.Mockito.doThrow
2525
import org.mockito.Mockito.mock
26+
import org.mockito.Mockito.clearInvocations
27+
import org.mockito.ArgumentMatchers.matches
28+
import org.mockito.Mockito.never
2629

2730
class IterableKeychainTest {
2831

@@ -202,8 +205,9 @@ class IterableKeychainTest {
202205
keychain.saveUserId(null)
203206
keychain.saveAuthToken(null)
204207

205-
// Verify exactly one putString call for each save operation
206-
verify(mockEditor, times(3)).putString(any(), isNull())
208+
// Verify remove calls for both key and plaintext flag
209+
verify(mockEditor, times(6)).remove(any()) // 2 removes per save (key + plaintext flag)
210+
verify(mockEditor, times(3)).remove(matches(".*_plaintext"))
207211
// Verify exactly one apply call for each save operation
208212
verify(mockEditor, times(3)).apply()
209213
}
@@ -285,4 +289,41 @@ class IterableKeychainTest {
285289
// Verify attemptMigration was called exactly once
286290
verify(mockMigrator, times(1)).attemptMigration()
287291
}
292+
293+
@Test
294+
fun testEncryptionAndDecryptionFailure() {
295+
// Setup encryption and decryption to fail
296+
`when`(mockEncryptor.encrypt(any())).thenThrow(RuntimeException("Simulated encryption failure"))
297+
`when`(mockEncryptor.decrypt(any())).thenThrow(RuntimeException("Simulated decryption failure"))
298+
299+
val testData = "test data for encryption failure"
300+
301+
// Save data - should fall back to plaintext
302+
keychain.saveUserId(testData)
303+
304+
// Verify plaintext save operations
305+
verify(mockEditor).putString(eq("iterable-user-id"), eq(testData))
306+
verify(mockEditor).putBoolean(eq("iterable-user-id_plaintext"), eq(true))
307+
308+
// Setup SharedPreferences to return the saved data
309+
`when`(mockSharedPrefs.getString(eq("iterable-user-id"), isNull())).thenReturn(testData)
310+
`when`(mockSharedPrefs.getBoolean(eq("iterable-user-id_plaintext"), eq(false))).thenReturn(true)
311+
312+
// Verify data can be retrieved
313+
assertEquals(testData, keychain.getUserId())
314+
315+
// Verify encryption was attempted and failed
316+
verify(mockEncryptor).encrypt(eq(testData))
317+
verify(mockEncryptor, never()).decrypt(any()) // Should not attempt decryption for plaintext
318+
319+
// Test null handling
320+
clearInvocations(mockEditor)
321+
keychain.saveUserId(null)
322+
verify(mockEditor).remove(eq("iterable-user-id"))
323+
verify(mockEditor).remove(eq("iterable-user-id_plaintext"))
324+
325+
// Verify no more encryption attempts for null
326+
verify(mockEncryptor, never()).encrypt(isNull())
327+
verify(mockEncryptor, never()).decrypt(isNull())
328+
}
288329
}

0 commit comments

Comments
 (0)