-
-
Save morristech/4baea0fd5d1329c457013e6b57fdaddd to your computer and use it in GitHub Desktop.
Cryptography - An object class for cryptography that encrypt/decrypt using AndroidKeyStore for Android 18+
This file contains hidden or 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
| package com.example.securitypractice | |
| import android.annotation.TargetApi | |
| import android.content.Context | |
| import android.content.SharedPreferences | |
| import android.os.Build | |
| import android.security.KeyPairGeneratorSpec | |
| import android.security.keystore.KeyGenParameterSpec | |
| import android.security.keystore.KeyProperties | |
| import android.util.Base64 | |
| import android.util.Log | |
| import java.math.BigInteger | |
| import java.security.KeyPairGenerator | |
| import java.security.KeyStore | |
| import java.security.interfaces.RSAPrivateKey | |
| import java.security.interfaces.RSAPublicKey | |
| import java.util.* | |
| import javax.crypto.Cipher | |
| import javax.crypto.KeyGenerator | |
| import javax.crypto.SecretKey | |
| import javax.crypto.spec.IvParameterSpec | |
| import javax.crypto.spec.SecretKeySpec | |
| import javax.security.auth.x500.X500Principal | |
| import kotlin.math.abs | |
| /** | |
| * The class is responsible for generating crypto keys [AES and RSA] in AndroidKeyStore | |
| * | |
| * It exposes only three methods to client: | |
| * - encrypt > to encrypt plain text format into cipher format | |
| * - decrypt > to decrypt cipher format into plain text | |
| * - initialize(Context) > must be called ONLY once (Ideally from Application class) before using | |
| * encrypt/decrypt as it is responsible to generate crypto keys according to OS level | |
| * (generate RSA key for Below Android M and AES key for Android M+ in AndroidKeyStore) as | |
| * Below Android M, AES key is not supported by AndroidKeyStore. So, RSA key will be used to | |
| * encrypt/decrypt the AES key for below Android M and then store in preferences to use later to | |
| * encrypt/decrypt the actual text data. Uses only AES key for encryption/decryption to the text. | |
| */ | |
| object Cryptography { | |
| private const val TAG = "CryptographyTAG" | |
| private const val KEY_SIZE = 256 | |
| private const val KEY_ALIAS = "MySecretKeyAlias" | |
| private const val KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore" | |
| private const val TYPE_AES = "AES" | |
| private const val TYPE_RSA = "RSA" | |
| private const val IV_SEPARATOR = "[IV_SEPARATE]" | |
| private const val SECRET_PREF = "SECRET_PREF" | |
| private const val SECRET_KEY_IN_PREF = "SECRET_KEY_IN_PREF" | |
| private const val RSA_PADDING = "RSA/NONE/PKCS1Padding" | |
| private const val AES_PADDING = "AES/CBC/NoPadding" | |
| private var secretKey: SecretKey? = null | |
| private var preferences: SharedPreferences? = null | |
| private var attemptToCreateSecretKey = false | |
| private var context: Context? = null | |
| /** | |
| * Generating keys [AES or RSA] according to Android OS level | |
| * | |
| * @param context Context | |
| */ | |
| fun initialize(context: Context) { | |
| this.context = context | |
| preferences = context.getSharedPreferences(SECRET_PREF, Context.MODE_PRIVATE) | |
| if (!isSigningKeyAvailable()) { | |
| if (isOSFromM()) { | |
| createSecretKeyForM() | |
| } else { | |
| createRSAKeys() | |
| } | |
| } | |
| secretKey = getSecretKey() | |
| } | |
| /** | |
| * Generating secret key for Android M+ in AndroidKeyStore | |
| */ | |
| @TargetApi(Build.VERSION_CODES.M) | |
| private fun createSecretKeyForM() { | |
| //instance of key generator with AES in AndroidKeyStore | |
| val keyGenerator = | |
| KeyGenerator.getInstance( | |
| KeyProperties.KEY_ALGORITHM_AES, | |
| KEYSTORE_PROVIDER_ANDROID_KEYSTORE | |
| ) | |
| //Reason to wrapping in try..catch is, setting StrongBox feature will throw [StrongBoxUnavailableException] | |
| //for the devices that doesn't support StrongBox feature for Android P+ | |
| //So, If Android P+ devices that doesn't support hardware base AndroidKeyStore will throw | |
| //exception in generating key. If exception occurs, we generate key without StrongBox in catch block | |
| try { | |
| val keyGenParameterSpec = getKeyGenParameterSpec(true) | |
| keyGenerator.init(keyGenParameterSpec) | |
| keyGenerator.generateKey() | |
| } catch (e: Exception) { | |
| e.printStackTrace() | |
| val keyGenParameterSpec = getKeyGenParameterSpec(false) | |
| keyGenerator.init(keyGenParameterSpec) | |
| keyGenerator.generateKey() | |
| } | |
| } | |
| /** | |
| * Generate secret keys specifications builder for Android M | |
| * | |
| * @param setStrongBox Boolean to set Strong box feature (Hardware based AndroidKeyStore) | |
| * for Android P+ | |
| * @return [KeyGenParameterSpec] Builder for secret key | |
| */ | |
| @TargetApi(Build.VERSION_CODES.M) | |
| private fun getKeyGenParameterSpec(setStrongBox: Boolean): KeyGenParameterSpec { | |
| val builder = KeyGenParameterSpec.Builder( | |
| KEY_ALIAS, | |
| KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT | |
| ) | |
| .setKeySize(KEY_SIZE) | |
| .setBlockModes(KeyProperties.BLOCK_MODE_CBC) | |
| .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) | |
| //setting strong box feature for android P+ | |
| if (isOSFromP()) | |
| builder.setIsStrongBoxBacked(setStrongBox) | |
| return builder.build() | |
| } | |
| //endregion | |
| /** | |
| * Getting secret (AES) key | |
| * | |
| * @return [SecretKey] generated secret key if available else null | |
| */ | |
| private fun getSecretKey(): SecretKey? { | |
| return if (isOSFromM()) { | |
| //Getting Secret Key from AndroidKeyStore for Android M+ | |
| getKeystore()?.let { | |
| val secretKeyEntry = it.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry | |
| secretKeyEntry.secretKey | |
| } | |
| } else { | |
| //Getting Secret Key stored in preferences else null | |
| preferences?.getString(SECRET_KEY_IN_PREF, null)?.let { encodedSecretKey -> | |
| //decode the secret key that was stored in preferences in Base64 encoded form | |
| val decodedSecretKey = Base64.decode(encodedSecretKey, Base64.DEFAULT) | |
| //decrypt the secret key after getting the decode form of secret key | |
| val decryptedSecretKey = decryptSecretKeyForPreM(decodedSecretKey) | |
| //Create secret key object | |
| SecretKeySpec(decodedSecretKey, 0, decryptedSecretKey.size, TYPE_AES) | |
| } ?: run { | |
| //Generate RSA keys if not generated before | |
| createRSAKeys() | |
| //checking if secret key finding attempted to avoid looping infinitely | |
| if (!attemptToCreateSecretKey) { | |
| attemptToCreateSecretKey = true | |
| getSecretKey() | |
| } else null | |
| } | |
| } | |
| } | |
| /** | |
| * Get keystore instance from AndroidKeyStore | |
| * | |
| * @return [KeyStore] Keystore instance if available else null | |
| */ | |
| private fun getKeystore(): KeyStore? = if (isSigningKeyAvailable()) { | |
| KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE).apply { | |
| load(null) | |
| } | |
| } else { | |
| null | |
| } | |
| /** | |
| * Generate secret (AES) key for below Android M | |
| */ | |
| private fun createSecretKeyForPreM() = KeyGenerator.getInstance(TYPE_AES).apply { | |
| init(KEY_SIZE) | |
| }.generateKey() | |
| /** | |
| * Generating RSA keys [Public and Private] inside AndroidKeyStore for below android M | |
| */ | |
| private fun createRSAKeys() { | |
| //certificate start time | |
| val start: Calendar = GregorianCalendar() | |
| //certificate end time | |
| val end: Calendar = GregorianCalendar() | |
| //30 years for expiration of certificate | |
| end.add(Calendar.YEAR, 30) | |
| //creating certificate specifications for key generation | |
| val spec = | |
| KeyPairGeneratorSpec.Builder(context!!) | |
| // for the key! | |
| .setAlias(KEY_ALIAS) | |
| .setSubject(X500Principal("CN=${KEY_ALIAS}")) | |
| .setSerialNumber( | |
| BigInteger.valueOf(abs(KEY_ALIAS.hashCode()).toLong()) | |
| ) // Date range of validity for the generated pair. | |
| .setStartDate(start.time) | |
| .setEndDate(end.time) | |
| .build() | |
| //get instance for AndroidKeyStore | |
| val kpGenerator = | |
| KeyPairGenerator.getInstance(TYPE_RSA, KEYSTORE_PROVIDER_ANDROID_KEYSTORE) | |
| //initialize for RSA key generation | |
| kpGenerator.initialize(spec) | |
| //generate RSA keys inside AndroidKeyStore | |
| kpGenerator.generateKeyPair() | |
| //Creating secret (AES) key for encryption/decryption, RSA will not be used for that | |
| val secretKey = createSecretKeyForPreM() | |
| Log.d(TAG, String(secretKey.encoded)) | |
| //Encrypt generated secret key | |
| val encryptedSecretKey = encryptSecretKeyForPreM(secretKey) | |
| //Encode encrypted secret key to Base64 | |
| val encodedSecretKey = Base64.encodeToString(encryptedSecretKey, Base64.DEFAULT) | |
| //store encrypted secret key in Base64 encoded form | |
| preferences?.edit()?.putString(SECRET_KEY_IN_PREF, encodedSecretKey)?.apply() | |
| } | |
| /** | |
| * Getting RSA keys [public and private] from keystore | |
| * | |
| * @return [Pair<RSAPrivateKey, RSAPublicKey>] pair of RSA keys | |
| */ | |
| private fun getRSAPairKeys(): Pair<RSAPrivateKey, RSAPublicKey> { | |
| val privateKeyEntry = getKeystore()?.getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry | |
| val privateKey = privateKeyEntry.privateKey as RSAPrivateKey | |
| val publicKey = privateKeyEntry.certificate.publicKey as RSAPublicKey | |
| Log.d(TAG, Base64.encodeToString(privateKeyEntry.certificate.encoded, Base64.DEFAULT)) | |
| return Pair(privateKey, publicKey) | |
| } | |
| /** | |
| * Encrypt AES key with RSA public key for below Android M | |
| * | |
| * @param secretKey plain AES key | |
| * @return [ByteArray] encrypted AES key | |
| */ | |
| private fun encryptSecretKeyForPreM(secretKey: SecretKey): ByteArray { | |
| val publicKey = getRSAPairKeys().second | |
| val cipher = Cipher.getInstance(RSA_PADDING) | |
| cipher.init(Cipher.ENCRYPT_MODE, publicKey) | |
| return cipher.doFinal(secretKey.encoded) | |
| } | |
| /** | |
| * Decrypt AES key with RSA private key for below Android M | |
| * | |
| * @param decodedSecretKey Base64 decoded [encrypted secret key] that was stored in preferences | |
| * in Base64 encoded form | |
| * @return [ByteArray] Decrypted secret key | |
| */ | |
| private fun decryptSecretKeyForPreM(decodedSecretKey: ByteArray): ByteArray { | |
| val privateKey = getRSAPairKeys().first | |
| val cipher = Cipher.getInstance(RSA_PADDING) | |
| cipher.init(Cipher.DECRYPT_MODE, privateKey) | |
| return cipher.doFinal(decodedSecretKey) | |
| } | |
| /** | |
| * Encrypt the plain text into cipher format | |
| * | |
| * @param plaintText plain text | |
| * @return [String] cipher return if the params and secret key are not null else plaintext | |
| */ | |
| fun encrypt(plaintText: String?): String? { | |
| return if (!plaintText.isNullOrEmpty() && secretKey != null) { | |
| var finalEncryptedText = "" | |
| val cipher = Cipher.getInstance(AES_PADDING) | |
| var temp: String = plaintText | |
| while (temp.toByteArray().size % KEY_SIZE != 0) { | |
| temp += "\u0020" | |
| } | |
| cipher.init(Cipher.ENCRYPT_MODE, secretKey) | |
| val ivBytes = cipher.iv | |
| finalEncryptedText += "${Base64.encodeToString(ivBytes, Base64.DEFAULT)}$IV_SEPARATOR" | |
| val encryptedBytes = cipher.doFinal(temp.toByteArray(Charsets.UTF_8)) | |
| finalEncryptedText += Base64.encodeToString(encryptedBytes, Base64.DEFAULT) | |
| finalEncryptedText | |
| } else plaintText | |
| } | |
| /** | |
| * Decrypt the cipher formatted text | |
| * | |
| * @param encryptedText cipher formatted text | |
| * @return [String] plaintext return if the params and secret key are not null else ciphered text | |
| */ | |
| fun decrypt(encryptedText: String?): String? { | |
| return if (!encryptedText.isNullOrEmpty() && secretKey != null) { | |
| val splitData = encryptedText.split(IV_SEPARATOR) | |
| val iv = Base64.decode(splitData[0], Base64.DEFAULT) | |
| val cipher = Cipher.getInstance(AES_PADDING) | |
| val spec = IvParameterSpec(iv) | |
| cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) | |
| return String(cipher.doFinal(Base64.decode(splitData[1], Base64.DEFAULT))) | |
| } else encryptedText | |
| } | |
| /** | |
| * If Key with the default alias exists, returns true, else false. | |
| * on pre-JBMR2 returns true always. | |
| */ | |
| private fun isSigningKeyAvailable(): Boolean { | |
| return if (isOSFrom18()) { | |
| try { | |
| val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE) | |
| keyStore.load(null) | |
| keyStore.containsAlias(KEY_ALIAS) | |
| } catch (e: java.lang.Exception) { | |
| Log.e(TAG, e.message, e) | |
| false | |
| } | |
| } else { | |
| false | |
| } | |
| } | |
| private fun isOSFrom18() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 | |
| private fun isOSFromM() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M | |
| private fun isOSFromP() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment