Skip to content

Instantly share code, notes, and snippets.

@FrancescoJo
Last active December 3, 2022 20:18
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save FrancescoJo/b306925d245f095c68655c4c40bb38f8 to your computer and use it in GitHub Desktop.
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)+
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