Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package com.mendix.mendixnative.encryption
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Base64.DEFAULT
import androidx.annotation.RequiresApi
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.io.IOException
Expand All @@ -17,10 +15,15 @@ import java.security.Key
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec

private const val STORE_AES_KEY = "AES_KEY"
private const val encryptionTransformationName = "AES/CBC/PKCS7Padding"
private const val STORE_AES_KEY_V2 = "AES_KEY_V2"
private const val legacyEncryptionTransformationName = "AES/CBC/PKCS7Padding"
private const val modernEncryptionTransformationName = "AES/GCM/NoPadding"
private const val modernEncryptionVersionPrefix = "v2:"
private const val gcmTagLengthBits = 128

private var masterKey: MasterKey? = null
fun getMasterKey(context: Context): MasterKey {
Expand Down Expand Up @@ -48,27 +51,41 @@ fun getEncryptedSharedPreferences(
}

/**
* generates or returns an application wide AES key.
* returns an application wide AES key.
*
* @return Key
*/
@RequiresApi(Build.VERSION_CODES.M)
private fun getAESKey(): Key? {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
if (!keyStore.containsAlias(STORE_AES_KEY)) {
return if (keyStore.containsAlias(STORE_AES_KEY))
keyStore.getKey(STORE_AES_KEY, null)
else null
}

private fun getAESKeyV2(): Key? {
return getOrCreateAESKey(
STORE_AES_KEY_V2,
KeyProperties.BLOCK_MODE_GCM,
KeyProperties.ENCRYPTION_PADDING_NONE
)
}

private fun getOrCreateAESKey(alias: String, blockMode: String, encryptionPadding: String): Key? {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
if (!keyStore.containsAlias(alias)) {
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder(
STORE_AES_KEY,
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).build()
.setBlockModes(blockMode)
.setEncryptionPaddings(encryptionPadding).build()
)
keyGenerator.generateKey()
}
return keyStore.getKey(STORE_AES_KEY, null)
return keyStore.getKey(alias, null)
}

/**
Expand All @@ -79,13 +96,15 @@ private fun getAESKey(): Key? {
*/
fun encryptValue(
value: String,
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() },
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKeyV2() },
): Triple<ByteArray, ByteArray?, Boolean> {
val cipher = Cipher.getInstance(encryptionTransformationName)
val cipher = Cipher.getInstance(modernEncryptionTransformationName)
cipher.init(Cipher.ENCRYPT_MODE, getPassword())
val encryptedValue = cipher.doFinal(value.encodeToByteArray())
val versionedEncryptedValue =
"$modernEncryptionVersionPrefix${Base64.encodeToString(encryptedValue, DEFAULT)}"
return Triple(
Base64.encode(encryptedValue, DEFAULT),
versionedEncryptedValue.encodeToByteArray(),
Base64.encode(cipher.iv, DEFAULT),
true
)
Expand All @@ -101,9 +120,23 @@ fun encryptValue(
fun decryptValue(
value: String,
iv: String?,
@SuppressLint("NewApi", "LocalSuppress") getPassword: () -> Key? = { getAESKey() },
@SuppressLint("NewApi", "LocalSuppress") legacyGetPassword: () -> Key? = { getAESKey() },
@SuppressLint("NewApi", "LocalSuppress") modernGetPassword: () -> Key? = { getAESKeyV2() },
): String {
return if (value.startsWith(modernEncryptionVersionPrefix)) {
decryptModernValue(value.removePrefix(modernEncryptionVersionPrefix), iv, modernGetPassword)
} else {
decryptLegacyValue(value, iv, legacyGetPassword)
}
}

private fun decryptLegacyValue(
value: String,
iv: String?,
getPassword: () -> Key?,
): String {
val cipher = Cipher.getInstance(encryptionTransformationName)
requireNotNull(iv) { "Missing IV for legacy encrypted value." }
val cipher = Cipher.getInstance(legacyEncryptionTransformationName)
cipher.init(
Cipher.DECRYPT_MODE,
getPassword(),
Expand All @@ -112,3 +145,19 @@ fun decryptValue(
val unencryptedValue = cipher.doFinal(Base64.decode(value, DEFAULT))
return String(unencryptedValue, Charsets.UTF_8)
}

private fun decryptModernValue(
value: String,
iv: String?,
getPassword: () -> Key?,
): String {
requireNotNull(iv) { "Missing nonce for modern encrypted value." }
val cipher = Cipher.getInstance(modernEncryptionTransformationName)
cipher.init(
Cipher.DECRYPT_MODE,
getPassword(),
GCMParameterSpec(gcmTagLengthBits, Base64.decode(iv, DEFAULT))
)
val unencryptedValue = cipher.doFinal(Base64.decode(value, DEFAULT))
return String(unencryptedValue, Charsets.UTF_8)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mendix.mendixnative.request

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

if (encryptedCookieExists!! && key.startsWith(encryptedCookieKeyPrefix)) {
val params = cookieValueToDecryptionParams(value)
val decryptedValue = decryptValue(params.first, params.second)

return@map "${key.removePrefix(encryptedCookieKeyPrefix)}=$decryptedValue"
try {
val params = cookieValueToDecryptionParams(value)
val decryptedValue = decryptValue(params.first, params.second)
return@map "${key.removePrefix(encryptedCookieKeyPrefix)}=$decryptedValue"
} catch (e: Exception) {
Log.w("MendixNetworkInterceptor", "Failed to decrypt cookie $key, dropping it", e)
return@map null
}
} else if (!encryptedCookieExists) {
return@map it;
}
Expand Down
Loading