Skip to content

Commit ebbe62c

Browse files
Merge branch 'main' into mx-native-revamp
2 parents 8af9fd5 + c3c4a9c commit ebbe62c

6 files changed

Lines changed: 81 additions & 25 deletions

File tree

.github/workflows/ios.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ jobs:
4141
example/ios/Pods
4242
~/Library/Caches/CocoaPods
4343
~/.cocoapods
44-
key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }}
45-
restore-keys: |
46-
${{ runner.os }}-pods-
44+
key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock', 'yarn.lock', 'package.json', 'example/package.json') }}
4745

4846
- name: Install CocoaPods
4947
working-directory: example

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v0.4.1] - 2026-04-22
11+
12+
- We strengthened Android cookie encryption by migrating from `AES/CBC/PKCS7Padding` to `AES/GCM/NoPadding`.
13+
1014
## [v0.4.0] - 2026-04-17
1115

1216
- We upgraded `@op-engineering/op-sqlite` from v15.0.7 to v15.2.5.

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
}

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ PODS:
33
- hermes-engine (0.14.1):
44
- hermes-engine/Pre-built (= 0.14.1)
55
- hermes-engine/Pre-built (0.14.1)
6-
- MendixNative (0.3.2):
6+
- MendixNative (0.4.0):
77
- hermes-engine
88
- RCTRequired
99
- RCTTypeSafety
@@ -2120,7 +2120,7 @@ EXTERNAL SOURCES:
21202120
SPEC CHECKSUMS:
21212121
FBLazyVector: 061f518bbd81677ed8a8317e2ae60b8779495808
21222122
hermes-engine: f8a008831ed2a6655a05eec287332a01c1c2e108
2123-
MendixNative: 2d6d84bc9ad47a4466ae2ab59d72e47ca396e623
2123+
MendixNative: d23548dd07ed06f0d6243ee934963715adbf215d
21242124
op-sqlite: e9ef65bcf95a97863874cee87841425bb71c8396
21252125
OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2
21262126
RCTDeprecation: 5045f20b2cc1239bf422764004338c720684a22f

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mendix-native",
3-
"version": "0.4.0",
3+
"version": "0.4.1",
44
"description": "Mendix native mobile package",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

0 commit comments

Comments
 (0)