Skip to content

Commit d7f71aa

Browse files
feat: enhance cookie encryption handling with versioning and error logging
1 parent 08f18f5 commit d7f71aa

2 files changed

Lines changed: 73 additions & 19 deletions

File tree

android/src/main/java/com/mendix/mendixnative/encryption/MendixEncryptionToolkit.kt

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ package com.mendix.mendixnative.encryption
33
import android.annotation.SuppressLint
44
import android.content.Context
55
import android.content.SharedPreferences
6-
import android.os.Build
76
import android.security.keystore.KeyGenParameterSpec
87
import android.security.keystore.KeyProperties
98
import android.util.Base64
109
import android.util.Base64.DEFAULT
11-
import androidx.annotation.RequiresApi
1210
import androidx.security.crypto.EncryptedSharedPreferences
1311
import androidx.security.crypto.MasterKey
1412
import java.io.IOException
@@ -17,10 +15,15 @@ import java.security.Key
1715
import java.security.KeyStore
1816
import javax.crypto.Cipher
1917
import javax.crypto.KeyGenerator
18+
import javax.crypto.spec.GCMParameterSpec
2019
import javax.crypto.spec.IvParameterSpec
2120

2221
private const val STORE_AES_KEY = "AES_KEY"
23-
private const val encryptionTransformationName = "AES/CBC/PKCS7Padding"
22+
private const val STORE_AES_KEY_V2 = "AES_KEY_V2"
23+
private const val legacyEncryptionTransformationName = "AES/CBC/PKCS7Padding"
24+
private const val modernEncryptionTransformationName = "AES/GCM/NoPadding"
25+
private const val modernEncryptionVersionPrefix = "v2:"
26+
private const val gcmTagLengthBits = 128
2427

2528
private var masterKey: MasterKey? = null
2629
fun getMasterKey(context: Context): MasterKey {
@@ -48,27 +51,41 @@ fun getEncryptedSharedPreferences(
4851
}
4952

5053
/**
51-
* generates or returns an application wide AES key.
54+
* returns an application wide AES key.
5255
*
5356
* @return Key
5457
*/
55-
@RequiresApi(Build.VERSION_CODES.M)
5658
private fun getAESKey(): Key? {
5759
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
58-
if (!keyStore.containsAlias(STORE_AES_KEY)) {
60+
return if (keyStore.containsAlias(STORE_AES_KEY))
61+
keyStore.getKey(STORE_AES_KEY, null)
62+
else null
63+
}
64+
65+
private fun getAESKeyV2(): Key? {
66+
return getOrCreateAESKey(
67+
STORE_AES_KEY_V2,
68+
KeyProperties.BLOCK_MODE_GCM,
69+
KeyProperties.ENCRYPTION_PADDING_NONE
70+
)
71+
}
72+
73+
private fun getOrCreateAESKey(alias: String, blockMode: String, encryptionPadding: String): Key? {
74+
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
75+
if (!keyStore.containsAlias(alias)) {
5976
val keyGenerator =
6077
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
6178
keyGenerator.init(
6279
KeyGenParameterSpec.Builder(
63-
STORE_AES_KEY,
80+
alias,
6481
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
6582
)
66-
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
67-
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).build()
83+
.setBlockModes(blockMode)
84+
.setEncryptionPaddings(encryptionPadding).build()
6885
)
6986
keyGenerator.generateKey()
7087
}
71-
return keyStore.getKey(STORE_AES_KEY, null)
88+
return keyStore.getKey(alias, null)
7289
}
7390

7491
/**
@@ -79,13 +96,15 @@ private fun getAESKey(): Key? {
7996
*/
8097
fun encryptValue(
8198
value: String,
82-
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() },
99+
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKeyV2() },
83100
): Triple<ByteArray, ByteArray?, Boolean> {
84-
val cipher = Cipher.getInstance(encryptionTransformationName)
101+
val cipher = Cipher.getInstance(modernEncryptionTransformationName)
85102
cipher.init(Cipher.ENCRYPT_MODE, getPassword())
86103
val encryptedValue = cipher.doFinal(value.encodeToByteArray())
104+
val versionedEncryptedValue =
105+
"$modernEncryptionVersionPrefix${Base64.encodeToString(encryptedValue, DEFAULT)}"
87106
return Triple(
88-
Base64.encode(encryptedValue, DEFAULT),
107+
versionedEncryptedValue.encodeToByteArray(),
89108
Base64.encode(cipher.iv, DEFAULT),
90109
true
91110
)
@@ -101,9 +120,23 @@ fun encryptValue(
101120
fun decryptValue(
102121
value: String,
103122
iv: String?,
104-
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() },
123+
@SuppressLint("NewApi", "LocalSuppress") legacyGetPassword: () -> Key? = { getAESKey() },
124+
@SuppressLint("NewApi", "LocalSuppress") modernGetPassword: () -> Key? = { getAESKeyV2() },
125+
): String {
126+
return if (value.startsWith(modernEncryptionVersionPrefix)) {
127+
decryptModernValue(value.removePrefix(modernEncryptionVersionPrefix), iv, modernGetPassword)
128+
} else {
129+
decryptLegacyValue(value, iv, legacyGetPassword)
130+
}
131+
}
132+
133+
private fun decryptLegacyValue(
134+
value: String,
135+
iv: String?,
136+
getPassword: () -> Key?,
105137
): String {
106-
val cipher = Cipher.getInstance(encryptionTransformationName)
138+
requireNotNull(iv) { "Missing IV for legacy encrypted value." }
139+
val cipher = Cipher.getInstance(legacyEncryptionTransformationName)
107140
cipher.init(
108141
Cipher.DECRYPT_MODE,
109142
getPassword(),
@@ -112,3 +145,19 @@ fun decryptValue(
112145
val unencryptedValue = cipher.doFinal(Base64.decode(value, DEFAULT))
113146
return String(unencryptedValue, Charsets.UTF_8)
114147
}
148+
149+
private fun decryptModernValue(
150+
value: String,
151+
iv: String?,
152+
getPassword: () -> Key?,
153+
): String {
154+
requireNotNull(iv) { "Missing nonce for modern encrypted value." }
155+
val cipher = Cipher.getInstance(modernEncryptionTransformationName)
156+
cipher.init(
157+
Cipher.DECRYPT_MODE,
158+
getPassword(),
159+
GCMParameterSpec(gcmTagLengthBits, Base64.decode(iv, DEFAULT))
160+
)
161+
val unencryptedValue = cipher.doFinal(Base64.decode(value, DEFAULT))
162+
return String(unencryptedValue, Charsets.UTF_8)
163+
}

android/src/main/java/com/mendix/mendixnative/request/MendixNetworkInterceptor.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mendix.mendixnative.request
22

3+
import android.util.Log
34
import com.mendix.mendixnative.config.AppUrl
45
import com.mendix.mendixnative.encryption.decryptValue
56
import com.mendix.mendixnative.encryption.encryptValue
@@ -39,10 +40,14 @@ fun Request.withDecryptedCookies(): Request {
3940
val (key, value) = it.split("=", limit = 2)
4041

4142
if (encryptedCookieExists!! && key.startsWith(encryptedCookieKeyPrefix)) {
42-
val params = cookieValueToDecryptionParams(value)
43-
val decryptedValue = decryptValue(params.first, params.second)
44-
45-
return@map "${key.removePrefix(encryptedCookieKeyPrefix)}=$decryptedValue"
43+
try {
44+
val params = cookieValueToDecryptionParams(value)
45+
val decryptedValue = decryptValue(params.first, params.second)
46+
return@map "${key.removePrefix(encryptedCookieKeyPrefix)}=$decryptedValue"
47+
} catch (e: Exception) {
48+
Log.w("MendixNetworkInterceptor", "Failed to decrypt cookie $key, dropping it", e)
49+
return@map null
50+
}
4651
} else if (!encryptedCookieExists) {
4752
return@map it;
4853
}

0 commit comments

Comments
 (0)