@@ -11,18 +11,21 @@ import javax.crypto.spec.GCMParameterSpec
11
11
import android.os.Build
12
12
import java.security.KeyStore.PasswordProtection
13
13
import androidx.annotation.VisibleForTesting
14
+ import java.security.SecureRandom
15
+ import javax.crypto.spec.IvParameterSpec
16
+ import android.annotation.TargetApi
14
17
15
18
class IterableDataEncryptor {
16
19
companion object {
17
20
private const val TAG = " IterableDataEncryptor"
18
21
private const val ANDROID_KEYSTORE = " AndroidKeyStore"
19
- private const val TRANSFORMATION = " AES/GCM/NoPadding"
22
+ private const val TRANSFORMATION_MODERN = " AES/GCM/NoPadding"
23
+ private const val TRANSFORMATION_LEGACY = " AES/CBC/PKCS5Padding"
20
24
private const val ITERABLE_KEY_ALIAS = " iterable_encryption_key"
21
- private const val GCM_IV_LENGTH = 12
22
25
private const val GCM_TAG_LENGTH = 128
26
+ private const val IV_LENGTH = 16
23
27
private val TEST_KEYSTORE_PASSWORD = " test_password" .toCharArray()
24
28
25
- // Make keyStore static so it's shared across instances
26
29
private val keyStore: KeyStore by lazy {
27
30
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .JELLY_BEAN_MR2 ) {
28
31
try {
@@ -62,28 +65,33 @@ class IterableDataEncryptor {
62
65
}
63
66
64
67
private fun canUseAndroidKeyStore (): Boolean {
65
- return Build .VERSION .SDK_INT >= Build .VERSION_CODES .JELLY_BEAN_MR2 &&
68
+ return Build .VERSION .SDK_INT >= Build .VERSION_CODES .M &&
66
69
keyStore.type == ANDROID_KEYSTORE
67
70
}
68
71
72
+ @TargetApi(Build .VERSION_CODES .M )
69
73
private fun generateAndroidKeyStoreKey (): Unit? {
70
74
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
75
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
76
+ val keyGenerator = KeyGenerator .getInstance(
77
+ KeyProperties .KEY_ALGORITHM_AES ,
78
+ ANDROID_KEYSTORE
79
+ )
80
+
81
+ val keySpec = KeyGenParameterSpec .Builder (
82
+ ITERABLE_KEY_ALIAS ,
83
+ KeyProperties .PURPOSE_ENCRYPT or KeyProperties .PURPOSE_DECRYPT
84
+ )
85
+ .setBlockModes(KeyProperties .BLOCK_MODE_GCM , KeyProperties .BLOCK_MODE_CBC )
86
+ .setEncryptionPaddings(KeyProperties .ENCRYPTION_PADDING_NONE , KeyProperties .ENCRYPTION_PADDING_PKCS7 )
87
+ .build()
88
+
89
+ keyGenerator.init (keySpec)
90
+ keyGenerator.generateKey()
91
+ Unit
92
+ } else {
93
+ null
94
+ }
87
95
} catch (e: Exception ) {
88
96
IterableLogger .e(TAG , " Failed to generate key using AndroidKeyStore" , e)
89
97
null
@@ -92,7 +100,7 @@ class IterableDataEncryptor {
92
100
93
101
private fun generateFallbackKey () {
94
102
val keyGenerator = KeyGenerator .getInstance(" AES" )
95
- keyGenerator.init (256 ) // 256-bit AES key
103
+ keyGenerator.init (256 )
96
104
val secretKey = keyGenerator.generateKey()
97
105
98
106
val keyEntry = KeyStore .SecretKeyEntry (secretKey)
@@ -113,31 +121,22 @@ class IterableDataEncryptor {
113
121
return (keyStore.getEntry(ITERABLE_KEY_ALIAS , protParam) as KeyStore .SecretKeyEntry ).secretKey
114
122
}
115
123
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
124
fun encrypt (value : String? ): String? {
128
125
if (value == null ) return null
129
126
130
127
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 ))
128
+ val data = value.toByteArray(Charsets .UTF_8 )
129
+ val encryptedData = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .KITKAT ) {
130
+ encryptModern(data)
131
+ } else {
132
+ encryptLegacy(data)
133
+ }
136
134
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)
135
+ // Combine isModern flag, IV, and encrypted data
136
+ val combined = ByteArray (1 + encryptedData.iv.size + encryptedData.data.size)
137
+ combined[0 ] = if (encryptedData.isModernEncryption) 1 else 0
138
+ System .arraycopy(encryptedData.iv, 0 , combined, 1 , encryptedData.iv.size)
139
+ System .arraycopy(encryptedData.data, 0 , combined, 1 + encryptedData.iv.size, encryptedData.data.size)
141
140
142
141
return Base64 .encodeToString(combined, Base64 .NO_WRAP )
143
142
} catch (e: Exception ) {
@@ -151,23 +150,101 @@ class IterableDataEncryptor {
151
150
152
151
try {
153
152
val combined = Base64 .decode(value, Base64 .NO_WRAP )
153
+
154
+ // Extract components
155
+ val isModern = combined[0 ] == 1 .toByte()
156
+ val iv = combined.copyOfRange(1 , 1 + IV_LENGTH )
157
+ val encrypted = combined.copyOfRange(1 + IV_LENGTH , combined.size)
158
+
159
+ val encryptedData = EncryptedData (encrypted, iv, isModern)
160
+
161
+ // If it's modern encryption and we're on an old device, fail fast
162
+ if (isModern && Build .VERSION .SDK_INT < Build .VERSION_CODES .KITKAT ) {
163
+ throw DecryptionException (" Modern encryption cannot be decrypted on legacy devices" )
164
+ }
154
165
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)
166
+ // Use the appropriate decryption method
167
+ val decrypted = if (isModern) {
168
+ decryptModern(encryptedData)
169
+ } else {
170
+ decryptLegacy(encryptedData)
171
+ }
162
172
163
- return String (cipher.doFinal(encrypted), Charsets .UTF_8 )
173
+ return String (decrypted, Charsets .UTF_8 )
174
+ } catch (e: DecryptionException ) {
175
+ // Re-throw DecryptionException directly
176
+ throw e
164
177
} catch (e: Exception ) {
165
178
IterableLogger .e(TAG , " Decryption failed" , e)
166
179
throw DecryptionException (" Failed to decrypt data" , e)
167
180
}
168
181
}
169
182
170
- // Add this method for testing purposes
183
+ @TargetApi(Build .VERSION_CODES .KITKAT )
184
+ private fun encryptModern (data : ByteArray ): EncryptedData {
185
+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .KITKAT ) {
186
+ return encryptLegacy(data)
187
+ }
188
+
189
+ val cipher = Cipher .getInstance(TRANSFORMATION_MODERN )
190
+ val iv = generateIV()
191
+ val spec = GCMParameterSpec (GCM_TAG_LENGTH , iv)
192
+ cipher.init (Cipher .ENCRYPT_MODE , getKey(), spec)
193
+ val encrypted = cipher.doFinal(data)
194
+ return EncryptedData (encrypted, iv, true )
195
+ }
196
+
197
+ private fun encryptLegacy (data : ByteArray ): EncryptedData {
198
+ val cipher = Cipher .getInstance(TRANSFORMATION_LEGACY )
199
+ val iv = generateIV()
200
+ val spec = IvParameterSpec (iv)
201
+ cipher.init (Cipher .ENCRYPT_MODE , getKey(), spec)
202
+ val encrypted = cipher.doFinal(data)
203
+ return EncryptedData (encrypted, iv, false )
204
+ }
205
+
206
+ @TargetApi(Build .VERSION_CODES .KITKAT )
207
+ private fun decryptModern (encryptedData : EncryptedData ): ByteArray {
208
+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .KITKAT ) {
209
+ throw DecryptionException (" Cannot decrypt modern encryption on legacy device" )
210
+ }
211
+
212
+ val cipher = Cipher .getInstance(TRANSFORMATION_MODERN )
213
+ val spec = GCMParameterSpec (GCM_TAG_LENGTH , encryptedData.iv)
214
+ cipher.init (Cipher .DECRYPT_MODE , getKey(), spec)
215
+ return cipher.doFinal(encryptedData.data)
216
+ }
217
+
218
+ private fun decryptLegacy (encryptedData : EncryptedData ): ByteArray {
219
+ val cipher = Cipher .getInstance(TRANSFORMATION_LEGACY )
220
+ val spec = IvParameterSpec (encryptedData.iv)
221
+ cipher.init (Cipher .DECRYPT_MODE , getKey(), spec)
222
+ return cipher.doFinal(encryptedData.data)
223
+ }
224
+
225
+ private fun generateIV (): ByteArray {
226
+ val iv = ByteArray (IV_LENGTH )
227
+ SecureRandom ().nextBytes(iv)
228
+ return iv
229
+ }
230
+
231
+ data class EncryptedData (
232
+ val data : ByteArray ,
233
+ val iv : ByteArray ,
234
+ val isModernEncryption : Boolean
235
+ )
236
+
237
+ class DecryptionException (message : String , cause : Throwable ? = null ) : Exception(message, cause)
238
+
239
+ fun resetKeys () {
240
+ try {
241
+ keyStore.deleteEntry(ITERABLE_KEY_ALIAS )
242
+ generateKey()
243
+ } catch (e: Exception ) {
244
+ IterableLogger .e(TAG , " Failed to regenerate key" , e)
245
+ }
246
+ }
247
+
171
248
@VisibleForTesting
172
249
fun getKeyStore (): KeyStore = keyStore
173
250
}
0 commit comments