Last active
December 3, 2022 20:18
-
-
Save FrancescoJo/b306925d245f095c68655c4c40bb38f8 to your computer and use it in GitHub Desktop.
Android AES cipher helper with system generated key. No more static final String key in our class - an approach which is very vulnerable to reverse engineering attack. Available only API Level 23(M)+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.TargetApi | |
import android.os.Build | |
import android.security.keystore.KeyGenParameterSpec | |
import android.security.keystore.KeyProperties | |
import android.util.Base64 | |
import timber.log.Timber | |
import java.security.GeneralSecurityException | |
import java.security.KeyStore | |
import javax.crypto.Cipher | |
import javax.crypto.KeyGenerator | |
import javax.crypto.SecretKey | |
import javax.crypto.spec.IvParameterSpec | |
/** | |
* String encryption/decryption helper with system generated key. | |
* | |
* Although using constant IV in CBC mode produces more more resilient results compare to ECB/CTR | |
* modes, it could increase chances of statistical crypto analysis attack and is not a good practice. | |
* Therefore, you must manage pairs of encrypted results and IV together to decrypt correctly. | |
* | |
* This class is marked as "Targeted for API Level 23+". Since system generated symmetric key is | |
* unfortunately, only supported in Android API Level 23+; For API Level 18 to 22, another key | |
* protection mechanism is required, such as a combination with asymmetric keys provided by | |
* Android KeyStore system. | |
* | |
* @author Francesco Jo(nimbusob@gmail.com) | |
* @since 03 - Jun - 2018 | |
*/ | |
@TargetApi(Build.VERSION_CODES.M) | |
object AndroidAesCipherHelper { | |
private const val KEY_LENGTH_BITS = 256 | |
private const val KEY_PROVIDER_NAME = "AndroidKeyStore" | |
private const val KEYGEN_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES | |
private const val KEYGEN_BLOCKMODE = KeyProperties.BLOCK_MODE_CBC | |
private const val KEYGEN_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 | |
private const val CIPHER_ALGORITHM = "$KEYGEN_ALGORITHM/$KEYGEN_BLOCKMODE/$KEYGEN_PADDING" | |
private lateinit var keyEntry: KeyStore.SecretKeyEntry | |
// Private only backing fields | |
@Suppress("ObjectPropertyName") | |
private lateinit var _keygen: KeyGenerator | |
// Private only backing fields | |
@Suppress("ObjectPropertyName") | |
private var _isSupported = false | |
val isSupported: Boolean | |
get() = _isSupported | |
internal fun init(applicationContext: Context) { | |
if (_isSupported) { | |
Timber.w("Already initialised - Do not attempt to initialise this twice") | |
return | |
} | |
try { | |
this._keygen = KeyGenerator.getInstance(KEYGEN_ALGORITHM, KEY_PROVIDER_NAME) | |
} catch (e: GeneralSecurityException) { | |
this._isSupported = false | |
// Nonsense, but happens on low-end devices such as Xiaomi | |
Timber.w("It seems that this device does not supports AES and/or RSA encryption.") | |
return | |
} | |
val alias = "${applicationContext.packageName}.aeskey" | |
val keyStore = KeyStore.getInstance(KEY_PROVIDER_NAME).apply({ | |
load(null) | |
}) | |
val result = if (keyStore.containsAlias(alias)) { | |
Timber.v("Secret key for %s exists, loading previously created one", alias) | |
true | |
} else { | |
Timber.v("No secret key for %s, creating a new one", alias) | |
initAndroidM(alias) | |
} | |
this.keyEntry = keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry | |
this._isSupported = result | |
} | |
private fun initAndroidM(alias: String): Boolean { | |
return try { | |
with(_keygen, { | |
init(KeyGenParameterSpec.Builder(alias, | |
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) | |
.setKeySize(KEY_LENGTH_BITS) | |
.setBlockModes(KEYGEN_BLOCKMODE) | |
.setEncryptionPaddings(KEYGEN_PADDING) | |
.build()) | |
generateKey() | |
}) | |
Timber.i("Secret key with %s is created.", CIPHER_ALGORITHM) | |
true | |
} catch (e: GeneralSecurityException) { | |
Timber.w(e, "It seems that this device does not support latest algorithm!!") | |
false | |
} | |
} | |
/** | |
* Note that backed result with empty IV means an operation failure. It is a good idea to | |
* check [isSupported] flag before invoking this method. | |
*/ | |
fun encrypt(plainText: String): EncryptionSpec { | |
if (!_isSupported) { | |
return EncryptionSpec(plainText, "") | |
} | |
val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({ | |
init(Cipher.ENCRYPT_MODE, keyEntry.secretKey) | |
}) | |
val result = cipher.doFinal(plainText.toByteArray()) | |
val iv = cipher.iv | |
return EncryptionSpec(result.toBase64String(), iv.toBase64String()) | |
} | |
/** | |
* This method assumes that all parameters are encoded as Base64 encoding. | |
*/ | |
fun decrypt(spec: EncryptionSpec): String { | |
if (!_isSupported) { | |
return spec.cipherText | |
} | |
val base64DecodedCipherText = Base64.decode(spec.cipherText, Base64.DEFAULT) | |
val base64DecodedIv = Base64.decode(spec.iv, Base64.DEFAULT) | |
val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({ | |
init(Cipher.DECRYPT_MODE, keyEntry.secretKey, IvParameterSpec(base64DecodedIv)) | |
}) | |
return String(cipher.doFinal(base64DecodedCipherText)) | |
} | |
private fun ByteArray.toBase64String(): String = | |
String(Base64.encode(this, Base64.DEFAULT)) | |
/** | |
* Generates a randomly chosen secure key for encryption. However, as long as the result of | |
* this method lives on memory, it could be a nice attack surface. | |
* | |
* Using this method is not good for a strong security measures, but if your 3rd party logic | |
* does not supports Android KeyStore, you have a no choice. | |
*/ | |
fun generateRandomKey(lengthBits: Int): ByteArray { | |
return with(KeyGenerator.getInstance("AES"), { | |
init(lengthBits, SecureRandom()) | |
generateKey().encoded | |
}) | |
} | |
/** | |
* All values of this class are Base64 encoded to ensure a safe Java String representation. | |
*/ | |
class EncryptionSpec(val cipherText: String, val iv: String) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment