@@ -11,18 +11,21 @@ import javax.crypto.spec.GCMParameterSpec
1111import android.os.Build
1212import java.security.KeyStore.PasswordProtection
1313import androidx.annotation.VisibleForTesting
14+ import java.security.SecureRandom
15+ import javax.crypto.spec.IvParameterSpec
16+ import android.annotation.TargetApi
1417
1518class IterableDataEncryptor {
1619 companion object {
1720 private const val TAG = " IterableDataEncryptor"
1821 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"
2024 private const val ITERABLE_KEY_ALIAS = " iterable_encryption_key"
21- private const val GCM_IV_LENGTH = 12
2225 private const val GCM_TAG_LENGTH = 128
26+ private const val IV_LENGTH = 16
2327 private val TEST_KEYSTORE_PASSWORD = " test_password" .toCharArray()
2428
25- // Make keyStore static so it's shared across instances
2629 private val keyStore: KeyStore by lazy {
2730 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .JELLY_BEAN_MR2 ) {
2831 try {
@@ -62,28 +65,33 @@ class IterableDataEncryptor {
6265 }
6366
6467 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 &&
6669 keyStore.type == ANDROID_KEYSTORE
6770 }
6871
72+ @TargetApi(Build .VERSION_CODES .M )
6973 private fun generateAndroidKeyStoreKey (): Unit? {
7074 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+ }
8795 } catch (e: Exception ) {
8896 IterableLogger .e(TAG , " Failed to generate key using AndroidKeyStore" , e)
8997 null
@@ -92,7 +100,7 @@ class IterableDataEncryptor {
92100
93101 private fun generateFallbackKey () {
94102 val keyGenerator = KeyGenerator .getInstance(" AES" )
95- keyGenerator.init (256 ) // 256-bit AES key
103+ keyGenerator.init (256 )
96104 val secretKey = keyGenerator.generateKey()
97105
98106 val keyEntry = KeyStore .SecretKeyEntry (secretKey)
@@ -113,31 +121,22 @@ class IterableDataEncryptor {
113121 return (keyStore.getEntry(ITERABLE_KEY_ALIAS , protParam) as KeyStore .SecretKeyEntry ).secretKey
114122 }
115123
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-
127124 fun encrypt (value : String? ): String? {
128125 if (value == null ) return null
129126
130127 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+ }
136134
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)
141140
142141 return Base64 .encodeToString(combined, Base64 .NO_WRAP )
143142 } catch (e: Exception ) {
@@ -151,23 +150,101 @@ class IterableDataEncryptor {
151150
152151 try {
153152 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+ }
154165
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+ }
162172
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
164177 } catch (e: Exception ) {
165178 IterableLogger .e(TAG , " Decryption failed" , e)
166179 throw DecryptionException (" Failed to decrypt data" , e)
167180 }
168181 }
169182
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+
171248 @VisibleForTesting
172249 fun getKeyStore (): KeyStore = keyStore
173250}
0 commit comments