Skip to content

Commit c44a2f5

Browse files
[SDK-365] Removed dead code from API 19 (#1046)
1 parent 440b44d commit c44a2f5

3 files changed

Lines changed: 38 additions & 281 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77
- Fixed `ConcurrentModificationException` crash during device token registration caused by concurrent access to `deviceAttributes`.
88
- Fixed possible `NoSuchMethodException` crash on Android 5-10 caused by using `Map.of()` which is unavailable on those versions
99

10+
### Removed
11+
- Removed insecure `AES/CBC/PKCS5Padding` encryption from `IterableDataEncryptor`. The SDK now exclusively uses `AES/GCM/NoPadding`. The legacy CBC algorithm was only used on Android versions below KitKat (API 19), which have been unsupported since `minSdkVersion` was raised to 21.
12+
1013
## [3.7.0]
1114
- Replaced the deprecated `AsyncTask`-based push notification handling with `WorkManager` for improved reliability and compatibility with modern Android versions. No action is required.
1215
- Fixed lost event tracking and missed API calls with an auto-retry feature for JWT token failures.

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

Lines changed: 27 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,26 @@ import javax.crypto.spec.GCMParameterSpec
1111
import android.os.Build
1212
import java.security.KeyStore.PasswordProtection
1313
import androidx.annotation.VisibleForTesting
14-
import java.security.SecureRandom
15-
import javax.crypto.spec.IvParameterSpec
1614
import android.annotation.TargetApi
1715

1816
class IterableDataEncryptor {
1917
companion object {
2018
private const val TAG = "IterableDataEncryptor"
2119
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
22-
private const val TRANSFORMATION_MODERN = "AES/GCM/NoPadding"
23-
private const val TRANSFORMATION_LEGACY = "AES/CBC/PKCS5Padding"
20+
private const val TRANSFORMATION = "AES/GCM/NoPadding"
2421
private const val ITERABLE_KEY_ALIAS = "iterable_encryption_key"
2522
private const val GCM_TAG_LENGTH = 128
26-
private const val GCM_IV_LENGTH = 12
27-
private const val CBC_IV_LENGTH = 16
28-
private val TEST_KEYSTORE_PASSWORD = "test_password".toCharArray()
23+
private val FALLBACK_KEYSTORE_PASSWORD = "test_password".toCharArray()
2924

3025
private val keyStore: KeyStore by lazy {
31-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
32-
try {
33-
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
34-
load(null)
35-
}
36-
} catch (e: Exception) {
37-
IterableLogger.e(TAG, "Failed to initialize AndroidKeyStore", e)
38-
KeyStore.getInstance("PKCS12").apply {
39-
load(null, TEST_KEYSTORE_PASSWORD)
40-
}
26+
try {
27+
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
28+
load(null)
4129
}
42-
} else {
30+
} catch (e: Exception) {
31+
IterableLogger.e(TAG, "Failed to initialize AndroidKeyStore", e)
4332
KeyStore.getInstance("PKCS12").apply {
44-
load(null, TEST_KEYSTORE_PASSWORD)
33+
load(null, FALLBACK_KEYSTORE_PASSWORD)
4534
}
4635
}
4736
}
@@ -83,8 +72,8 @@ class IterableDataEncryptor {
8372
ITERABLE_KEY_ALIAS,
8473
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
8574
)
86-
.setBlockModes(KeyProperties.BLOCK_MODE_GCM, KeyProperties.BLOCK_MODE_CBC)
87-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE, KeyProperties.ENCRYPTION_PADDING_PKCS7)
75+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
76+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
8877
.build()
8978

9079
keyGenerator.init(keySpec)
@@ -106,7 +95,7 @@ class IterableDataEncryptor {
10695

10796
val keyEntry = KeyStore.SecretKeyEntry(secretKey)
10897
val protParam = if (keyStore.type == "PKCS12") {
109-
PasswordProtection(TEST_KEYSTORE_PASSWORD)
98+
PasswordProtection(FALLBACK_KEYSTORE_PASSWORD)
11099
} else {
111100
null
112101
}
@@ -115,7 +104,7 @@ class IterableDataEncryptor {
115104

116105
private fun getKey(): SecretKey {
117106
val protParam = if (keyStore.type == "PKCS12") {
118-
PasswordProtection(TEST_KEYSTORE_PASSWORD)
107+
PasswordProtection(FALLBACK_KEYSTORE_PASSWORD)
119108
} else {
120109
null
121110
}
@@ -127,18 +116,17 @@ class IterableDataEncryptor {
127116

128117
try {
129118
val data = value.toByteArray(Charsets.UTF_8)
130-
val encryptedData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
131-
encryptModern(data)
132-
} else {
133-
encryptLegacy(data)
134-
}
135119

136-
// Combine isModern flag, IV length, IV, and encrypted data
137-
val combined = ByteArray(1 + 1 + encryptedData.iv.size + encryptedData.data.size)
138-
combined[0] = if (encryptedData.isModernEncryption) 1 else 0
139-
combined[1] = encryptedData.iv.size.toByte() // Store IV length
140-
System.arraycopy(encryptedData.iv, 0, combined, 2, encryptedData.iv.size)
141-
System.arraycopy(encryptedData.data, 0, combined, 2 + encryptedData.iv.size, encryptedData.data.size)
120+
val cipher = Cipher.getInstance(TRANSFORMATION)
121+
cipher.init(Cipher.ENCRYPT_MODE, getKey())
122+
val iv = cipher.iv
123+
val encrypted = cipher.doFinal(data)
124+
125+
val combined = ByteArray(1 + 1 + iv.size + encrypted.size)
126+
combined[0] = 1 // GCM flag (kept for format compatibility)
127+
combined[1] = iv.size.toByte()
128+
System.arraycopy(iv, 0, combined, 2, iv.size)
129+
System.arraycopy(encrypted, 0, combined, 2 + iv.size, encrypted.size)
142130

143131
return Base64.encodeToString(combined, Base64.NO_WRAP)
144132
} catch (e: Exception) {
@@ -152,91 +140,25 @@ class IterableDataEncryptor {
152140

153141
try {
154142
val combined = Base64.decode(value, Base64.NO_WRAP)
155-
156-
// Extract components
157-
val isModern = combined[0] == 1.toByte()
143+
158144
val ivLength = combined[1].toInt()
159145
val iv = combined.copyOfRange(2, 2 + ivLength)
160146
val encrypted = combined.copyOfRange(2 + ivLength, combined.size)
161147

162-
val encryptedData = EncryptedData(encrypted, iv, isModern)
163-
164-
// If it's modern encryption and we're on an old device, fail fast
165-
if (isModern && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
166-
throw DecryptionException("Modern encryption cannot be decrypted on legacy devices")
167-
}
168-
169-
// Use the appropriate decryption method
170-
val decrypted = if (isModern) {
171-
decryptModern(encryptedData)
172-
} else {
173-
decryptLegacy(encryptedData)
174-
}
148+
val cipher = Cipher.getInstance(TRANSFORMATION)
149+
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
150+
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
151+
val decrypted = cipher.doFinal(encrypted)
175152

176153
return String(decrypted, Charsets.UTF_8)
177154
} catch (e: DecryptionException) {
178-
// Re-throw DecryptionException directly
179155
throw e
180156
} catch (e: Exception) {
181157
IterableLogger.e(TAG, "Decryption failed", e)
182158
throw DecryptionException("Failed to decrypt data", e)
183159
}
184160
}
185161

186-
@TargetApi(Build.VERSION_CODES.KITKAT)
187-
private fun encryptModern(data: ByteArray): EncryptedData {
188-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
189-
return encryptLegacy(data)
190-
}
191-
192-
val cipher = Cipher.getInstance(TRANSFORMATION_MODERN)
193-
cipher.init(Cipher.ENCRYPT_MODE, getKey())
194-
val iv = cipher.iv
195-
val encrypted = cipher.doFinal(data)
196-
return EncryptedData(encrypted, iv, true)
197-
}
198-
199-
private fun encryptLegacy(data: ByteArray): EncryptedData {
200-
val cipher = Cipher.getInstance(TRANSFORMATION_LEGACY)
201-
val iv = generateIV(isModern = false)
202-
val spec = IvParameterSpec(iv)
203-
cipher.init(Cipher.ENCRYPT_MODE, getKey(), spec)
204-
val encrypted = cipher.doFinal(data)
205-
return EncryptedData(encrypted, iv, false)
206-
}
207-
208-
@TargetApi(Build.VERSION_CODES.KITKAT)
209-
private fun decryptModern(encryptedData: EncryptedData): ByteArray {
210-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
211-
throw DecryptionException("Cannot decrypt modern encryption on legacy device")
212-
}
213-
214-
val cipher = Cipher.getInstance(TRANSFORMATION_MODERN)
215-
val spec = GCMParameterSpec(GCM_TAG_LENGTH, encryptedData.iv)
216-
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
217-
return cipher.doFinal(encryptedData.data)
218-
}
219-
220-
private fun decryptLegacy(encryptedData: EncryptedData): ByteArray {
221-
val cipher = Cipher.getInstance(TRANSFORMATION_LEGACY)
222-
val spec = IvParameterSpec(encryptedData.iv)
223-
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
224-
return cipher.doFinal(encryptedData.data)
225-
}
226-
227-
private fun generateIV(isModern: Boolean = false): ByteArray {
228-
val length = if (isModern) GCM_IV_LENGTH else CBC_IV_LENGTH
229-
val iv = ByteArray(length)
230-
SecureRandom().nextBytes(iv)
231-
return iv
232-
}
233-
234-
data class EncryptedData(
235-
val data: ByteArray,
236-
val iv: ByteArray,
237-
val isModernEncryption: Boolean
238-
)
239-
240162
class DecryptionException(message: String, cause: Throwable? = null) : Exception(message, cause)
241163

242164
fun resetKeys() {

0 commit comments

Comments
 (0)