Skip to content

Commit 067c2f3

Browse files
authored
[MOB-9235] ANR Prevention by migrating data from encrypted to shared prefs and discontinue use of encrypted shared prefs (#849)
* [MOB-9235] Use shared prefs and try to migrate date from encrypted to shared prefs * [MOB-9235 Always encrypting * [MOB-9235 Always encrypting * [MOB-9235 Always encrypting * [MOB-9235 Always encrypting * [MOB-9235] Always encrypting * [MOB-9235] Always encrypting * [MOB-9235] Added decryption failure handler to IterableConfig * [MOB-9235] Added tests for encryptor * [MOB-9235] added some more tests for encryptor * [MOB-9235] added some more tests for encryptor * [MOB-9235] added some more tests for encryptor * [MOB-9235] added some more tests for migrator * [MOB-9235] added some more tests for keychain * [MOB-9235] added some more tests for keychain * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-10402] no migration in tests * [MOB-10402] no migration in tests * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Better Logging * [MOB-9235] Fixes * [MOB-9234] Make the migration blocking * [MOB-9235] Fix * [MOB-9235] Fix * [MOB-9235] Fix Test * [MOB-9235] Fix * [MOB-9235] Lint * [MOB-9235] Lint * [MOB-9235] Cleanup * [MOB-9235] Handle older versions * [MOB-9235] Cleanup * [MOB-9235] Cleanup * [MOB-9235] Cleanup
1 parent 8fdec83 commit 067c2f3

11 files changed

+1290
-150
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
## [Unreleased]
66

77
#### Added
8-
- nothing yet
8+
- - Added `IterableDecryptionFailureHandler` interface to handle decryption failures of PII information.
99

1010
#### Removed
11-
- nothing yet
11+
- Removed `encryptionEnforced` parameter from `IterableConfig` as data is now always encoded for security
1212

1313
#### Changed
14-
- nothing yet
14+
- Migrated from EncryptedSharedPreferences to regular SharedPreferences to prevent ANRs while EncryptedSharedPreferences was created on the main thread. We are now using our own encryption library to encrypt PII information before storing it in SharedPreferences.
1515

1616
## [3.5.4]
1717
#### Fixed

iterableapi/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ dependencies {
6565
testImplementation 'androidx.test.ext:junit:1.1.5'
6666
testImplementation 'androidx.test:rules:1.5.0'
6767
testImplementation 'org.mockito:mockito-core:3.3.3'
68-
testImplementation 'org.mockito:mockito-inline:2.8.47'
68+
testImplementation 'org.mockito:mockito-inline:5.2.0'
6969
testImplementation 'org.robolectric:robolectric:4.9.2'
7070
testImplementation 'org.robolectric:shadows-playservices:4.9.2'
7171
testImplementation 'org.khronos:opengl-api:gl1.1-android-2.1_r1'

iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ IterableKeychain getKeychain() {
143143
}
144144
if (keychain == null) {
145145
try {
146-
keychain = new IterableKeychain(getMainActivityContext(), config.encryptionEnforced);
146+
keychain = new IterableKeychain(getMainActivityContext(), config.decryptionFailureHandler);
147147
} catch (Exception e) {
148148
IterableLogger.e(TAG, "Failed to create IterableKeychain", e);
149149
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ public class IterableConfig {
8686
* By default, the SDK will save in-apps to disk.
8787
*/
8888
final boolean useInMemoryStorageForInApps;
89-
90-
final boolean encryptionEnforced;
91-
9289
/**
9390
* Allows for fetching embedded messages.
9491
*/
9592
final boolean enableEmbeddedMessaging;
9693

94+
/**
95+
* Handler for decryption failures of PII information.
96+
* Before calling this handler, the SDK will clear the PII information and create new encryption keys
97+
*/
98+
final IterableDecryptionFailureHandler decryptionFailureHandler;
99+
97100
private IterableConfig(Builder builder) {
98101
pushIntegrationName = builder.pushIntegrationName;
99102
urlHandler = builder.urlHandler;
@@ -109,8 +112,8 @@ private IterableConfig(Builder builder) {
109112
allowedProtocols = builder.allowedProtocols;
110113
dataRegion = builder.dataRegion;
111114
useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps;
112-
encryptionEnforced = builder.encryptionEnforced;
113115
enableEmbeddedMessaging = builder.enableEmbeddedMessaging;
116+
decryptionFailureHandler = builder.decryptionFailureHandler;
114117
}
115118

116119
public static class Builder {
@@ -128,8 +131,8 @@ public static class Builder {
128131
private String[] allowedProtocols = new String[0];
129132
private IterableDataRegion dataRegion = IterableDataRegion.US;
130133
private boolean useInMemoryStorageForInApps = false;
131-
private boolean encryptionEnforced = false;
132134
private boolean enableEmbeddedMessaging = false;
135+
private IterableDecryptionFailureHandler decryptionFailureHandler;
133136

134137
public Builder() {}
135138

@@ -261,17 +264,6 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) {
261264
return this;
262265
}
263266

264-
/**
265-
* Set whether the SDK should enforce encryption. If set to `true`, the SDK will not use fallback mechanism
266-
* of storing data in un-encrypted shared preferences if encrypted database is not available. Set this to `true`
267-
* if PII confidentiality is a concern for your app.
268-
* @param encryptionEnforced `true` will have the SDK enforce encryption.
269-
*/
270-
public Builder setEncryptionEnforced(boolean encryptionEnforced) {
271-
this.encryptionEnforced = encryptionEnforced;
272-
return this;
273-
}
274-
275267
/**
276268
* Set the data region used by the SDK
277269
* @param dataRegion enum value that determines which endpoint to use, defaults to IterableDataRegion.US
@@ -302,6 +294,16 @@ public Builder setEnableEmbeddedMessaging(boolean enableEmbeddedMessaging) {
302294
return this;
303295
}
304296

297+
/**
298+
* Set a handler for decryption failures that can be used to handle data recovery
299+
* @param handler Decryption failure handler provided by the app
300+
*/
301+
@NonNull
302+
public Builder setDecryptionFailureHandler(@NonNull IterableDecryptionFailureHandler handler) {
303+
this.decryptionFailureHandler = handler;
304+
return this;
305+
}
306+
305307
@NonNull
306308
public IterableConfig build() {
307309
return new IterableConfig(this);
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.iterable.iterableapi
2+
3+
import android.util.Base64
4+
import android.security.keystore.KeyGenParameterSpec
5+
import android.security.keystore.KeyProperties
6+
import java.security.KeyStore
7+
import javax.crypto.Cipher
8+
import javax.crypto.KeyGenerator
9+
import javax.crypto.SecretKey
10+
import javax.crypto.spec.GCMParameterSpec
11+
import android.os.Build
12+
import java.security.KeyStore.PasswordProtection
13+
import androidx.annotation.VisibleForTesting
14+
15+
class IterableDataEncryptor {
16+
companion object {
17+
private const val TAG = "IterableDataEncryptor"
18+
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
19+
private const val TRANSFORMATION = "AES/GCM/NoPadding"
20+
private const val ITERABLE_KEY_ALIAS = "iterable_encryption_key"
21+
private const val GCM_IV_LENGTH = 12
22+
private const val GCM_TAG_LENGTH = 128
23+
private val TEST_KEYSTORE_PASSWORD = "test_password".toCharArray()
24+
25+
// Make keyStore static so it's shared across instances
26+
private val keyStore: KeyStore by lazy {
27+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
28+
try {
29+
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
30+
load(null)
31+
}
32+
} catch (e: Exception) {
33+
IterableLogger.e(TAG, "Failed to initialize AndroidKeyStore", e)
34+
KeyStore.getInstance("PKCS12").apply {
35+
load(null, TEST_KEYSTORE_PASSWORD)
36+
}
37+
}
38+
} else {
39+
KeyStore.getInstance("PKCS12").apply {
40+
load(null, TEST_KEYSTORE_PASSWORD)
41+
}
42+
}
43+
}
44+
}
45+
46+
init {
47+
if (!keyStore.containsAlias(ITERABLE_KEY_ALIAS)) {
48+
generateKey()
49+
}
50+
}
51+
52+
private fun generateKey() {
53+
try {
54+
if (canUseAndroidKeyStore()) {
55+
generateAndroidKeyStoreKey()?.let { return }
56+
}
57+
generateFallbackKey()
58+
} catch (e: Exception) {
59+
IterableLogger.e(TAG, "Failed to generate key", e)
60+
throw e
61+
}
62+
}
63+
64+
private fun canUseAndroidKeyStore(): Boolean {
65+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
66+
keyStore.type == ANDROID_KEYSTORE
67+
}
68+
69+
private fun generateAndroidKeyStoreKey(): Unit? {
70+
return try {
71+
val keyGenerator = KeyGenerator.getInstance(
72+
KeyProperties.KEY_ALGORITHM_AES,
73+
ANDROID_KEYSTORE
74+
)
75+
76+
val keySpec = KeyGenParameterSpec.Builder(
77+
ITERABLE_KEY_ALIAS,
78+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
79+
)
80+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
81+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
82+
.build()
83+
84+
keyGenerator.init(keySpec)
85+
keyGenerator.generateKey()
86+
Unit
87+
} catch (e: Exception) {
88+
IterableLogger.e(TAG, "Failed to generate key using AndroidKeyStore", e)
89+
null
90+
}
91+
}
92+
93+
private fun generateFallbackKey() {
94+
val keyGenerator = KeyGenerator.getInstance("AES")
95+
keyGenerator.init(256) // 256-bit AES key
96+
val secretKey = keyGenerator.generateKey()
97+
98+
val keyEntry = KeyStore.SecretKeyEntry(secretKey)
99+
val protParam = if (keyStore.type == "PKCS12") {
100+
PasswordProtection(TEST_KEYSTORE_PASSWORD)
101+
} else {
102+
null
103+
}
104+
keyStore.setEntry(ITERABLE_KEY_ALIAS, keyEntry, protParam)
105+
}
106+
107+
private fun getKey(): SecretKey {
108+
val protParam = if (keyStore.type == "PKCS12") {
109+
PasswordProtection(TEST_KEYSTORE_PASSWORD)
110+
} else {
111+
null
112+
}
113+
return (keyStore.getEntry(ITERABLE_KEY_ALIAS, protParam) as KeyStore.SecretKeyEntry).secretKey
114+
}
115+
116+
class DecryptionException(message: String, cause: Throwable? = null) : Exception(message, cause)
117+
118+
fun resetKeys() {
119+
try {
120+
keyStore.deleteEntry(ITERABLE_KEY_ALIAS)
121+
generateKey()
122+
} catch (e: Exception) {
123+
IterableLogger.e(TAG, "Failed to regenerate key", e)
124+
}
125+
}
126+
127+
fun encrypt(value: String?): String? {
128+
if (value == null) return null
129+
130+
try {
131+
val cipher = Cipher.getInstance(TRANSFORMATION)
132+
cipher.init(Cipher.ENCRYPT_MODE, getKey())
133+
134+
val iv = cipher.iv
135+
val encrypted = cipher.doFinal(value.toByteArray(Charsets.UTF_8))
136+
137+
// Combine IV and encrypted data
138+
val combined = ByteArray(iv.size + encrypted.size)
139+
System.arraycopy(iv, 0, combined, 0, iv.size)
140+
System.arraycopy(encrypted, 0, combined, iv.size, encrypted.size)
141+
142+
return Base64.encodeToString(combined, Base64.NO_WRAP)
143+
} catch (e: Exception) {
144+
IterableLogger.e(TAG, "Encryption failed", e)
145+
throw e
146+
}
147+
}
148+
149+
fun decrypt(value: String?): String? {
150+
if (value == null) return null
151+
152+
try {
153+
val combined = Base64.decode(value, Base64.NO_WRAP)
154+
155+
// Extract IV
156+
val iv = combined.copyOfRange(0, GCM_IV_LENGTH)
157+
val encrypted = combined.copyOfRange(GCM_IV_LENGTH, combined.size)
158+
159+
val cipher = Cipher.getInstance(TRANSFORMATION)
160+
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
161+
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
162+
163+
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
164+
} catch (e: Exception) {
165+
IterableLogger.e(TAG, "Decryption failed", e)
166+
throw DecryptionException("Failed to decrypt data", e)
167+
}
168+
}
169+
170+
// Add this method for testing purposes
171+
@VisibleForTesting
172+
fun getKeyStore(): KeyStore = keyStore
173+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.iterable.iterableapi;
2+
3+
/**
4+
* Interface for handling decryption failures
5+
*/
6+
public interface IterableDecryptionFailureHandler {
7+
/**
8+
* Called when a decryption failure occurs
9+
* @param exception The exception that caused the decryption failure
10+
*/
11+
void onDecryptionFailed(Exception exception);
12+
}

0 commit comments

Comments
 (0)