@@ -3,12 +3,10 @@ package com.mendix.mendixnative.encryption
33import android.annotation.SuppressLint
44import android.content.Context
55import android.content.SharedPreferences
6- import android.os.Build
76import android.security.keystore.KeyGenParameterSpec
87import android.security.keystore.KeyProperties
98import android.util.Base64
109import android.util.Base64.DEFAULT
11- import androidx.annotation.RequiresApi
1210import androidx.security.crypto.EncryptedSharedPreferences
1311import androidx.security.crypto.MasterKey
1412import java.io.IOException
@@ -17,10 +15,15 @@ import java.security.Key
1715import java.security.KeyStore
1816import javax.crypto.Cipher
1917import javax.crypto.KeyGenerator
18+ import javax.crypto.spec.GCMParameterSpec
2019import javax.crypto.spec.IvParameterSpec
2120
2221private 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
2528private var masterKey: MasterKey ? = null
2629fun 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 )
5658private 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 */
8097fun 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(
101120fun 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+ }
0 commit comments