Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
725 views
in Technique[技术] by (71.8m points)

security - Android Keystore in kotlin

I'm working on an app that needs to be quite secure, so we want to use the Android Keystore to help make certain things, like accessTokens to the backend, safer and more secure.

I've tried implementing some of it, and it seems like it works, but I worked mainly off of examples, primarily Securely Storing Keys in Android Keystore

Here is some of my code:

object AndroidKeyStore {
const val ANDROID_KEY_STORE = "AndroidKeyStore"
const val AES_MODE = "AES/GCM/NoPadding"
private var iv: ByteArray = byteArrayOf(55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44)

//IS IT SAFE TO HAVE THIS HERE?
const val SECRET_ALIAS = "TEST"

private fun generateSecretKey(keyAlias: String): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
    val spec = KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setRandomizedEncryptionRequired(false)
            .build()

    keyGenerator.init(spec)
    return keyGenerator.generateKey()
}

private fun getSecretKey(keyAlias: String): SecretKey {
    val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
    if (keyStore.getEntry(keyAlias, null) != null) {
        val secretKeyEntry = keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry
        return secretKeyEntry.secretKey ?: generateSecretKey(keyAlias)
    }
    return generateSecretKey(keyAlias)
}

fun encrypt(data: String): String {
    val cipher = Cipher.getInstance(AES_MODE)
    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(SECRET_ALIAS), GCMParameterSpec(128, iv))
    iv = cipher.iv
    val encodedBytes = cipher.doFinal(data.toByteArray())
    return Base64.encodeToString(encodedBytes, Base64.NO_WRAP)
}

fun decrypt(encrypted: String): String {
    val cipher = Cipher.getInstance(AES_MODE)
    val spec = GCMParameterSpec(128, iv)
    cipher.init(Cipher.DECRYPT_MODE, getSecretKey(SECRET_ALIAS), spec)
    val encodedBytes = Base64.decode(encrypted, Base64.NO_WRAP)
    val decoded = cipher.doFinal(encodedBytes)
    return String(decoded, Charsets.UTF_8)
}

}

This is my keystore code, that seems like it encrypts/decrypts stuff okay. But I have a hard time understanding exactly what the Secret alias is and where to put it, safely.

At the same time, I'm also using a secure preference store. So my idea was, to keep the accessToken in the secure preference store, but encrypt it with this keystore code.

Here is the code for the Secure pref store.

object SecurePreferenceUtils {

enum class SecurePreferenceKeys {
    AccessToken, Test
}

fun putString(key: SecurePreferenceKeys, value: String) {
    SecurePrefs.securePreferences.edit().putString(key.name, value).commit()
}

fun getString(key: SecurePreferenceKeys, defaultValue: String): String {
    return SecurePrefs.securePreferences.getString(key.name, defaultValue) ?: ""
}

object SecurePrefs {
lateinit var securePreferences: SecurePreferences
}

In the end, I would like to be able to do like this when I get the accessToken from the backend.

SecurePreferenceUtils.putString(SecurePreferenceUtils.SecurePreferenceKeys.AccessToken, AndroidKeyStore.encrypt(""))

val token = AndroidKeyStore.decrypt(SecurePreferenceUtils.getString(SecurePreferenceUtils.SecurePreferenceKeys.AccessToken, AndroidKeyStore.encrypt("")))

Hopefully I make some sort of sense on where I want to go, I'm just not quite sure I'm on the right path here.

Any help is appreciated!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

alias is unique to one key, if you want to store username and password, you need 2 unique aliases

Here I'm getting it from resources xml file It is any unique string value

<resources>
    <string name="app_name">Data_Persistent</string>
    <string name="app_package">com.aprorit.keystoreexample</string>
</resources>

If SDK is less than 18, here I'm using shared preferences since Keystore available 18 upward. And decoding to base64 and store it

If SDK is 18 and above, use Keystore

This is how you access it

private const val PASSWORD_KEY = "password"
    private val passwordStorage: PasswordStorageHelper = PasswordStorageHelper(context)

    fun savePassword(password: String) {
        passwordStorage.setData(PASSWORD_KEY, password.toByteArray())
    }

    fun getPassword() : String {
        return String((passwordStorage.getData(PASSWORD_KEY) ?: ByteArray(0)))
    }

    fun removePassword() {
        passwordStorage.remove(PASSWORD_KEY)
    }

This is the PasswordStorageHelper class that takes care of pretty much everything

import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.security.KeyChain
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyInfo
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import com.example.data_persistent.R
import java.io.IOException
import java.math.BigInteger
import java.security.*
import java.security.cert.CertificateException
import java.security.spec.AlgorithmParameterSpec
import java.security.spec.InvalidKeySpecException
import java.util.*
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.security.auth.x500.X500Principal


class PasswordStorageHelper(context: Context) {
    private val tag = "PasswordStorageHelper"

    private val PREFS_NAME = "SecureData"

    private var passwordStorage: PasswordStorageInterface?

    init {
        passwordStorage = if (Build.VERSION.SDK_INT < 18) {
            PasswordStorageHelperSDK16();
        } else {
            PasswordStorageHelperSDK18();
        }

        var isInitialized: Boolean? = false;

        try {
            isInitialized = passwordStorage?.init(context);
        } catch (ex: Exception) {
            Log.e(tag, "PasswordStorage initialisation error:" + ex.message, ex);
        }

        if (isInitialized != true && passwordStorage is PasswordStorageHelperSDK18) {
            passwordStorage = PasswordStorageHelperSDK16();
            passwordStorage?.init(context);
        }
    }


    fun setData(key: String?, data: ByteArray?) {
        passwordStorage?.setData(key!!, data ?: ByteArray(0))
    }

    fun getData(key: String?): ByteArray? {
        return passwordStorage?.getData(key ?: "")
    }

    fun remove(key: String?) {
        passwordStorage?.remove(key ?: "")
    }

    private interface PasswordStorageInterface {
        fun init(context: Context?): Boolean
        fun setData(key: String?, data: ByteArray?)
        fun getData(key: String?): ByteArray?
        fun remove(key: String?)
    }

    private inner class PasswordStorageHelperSDK16 : PasswordStorageInterface {
        private var preferences: SharedPreferences? = null

        override fun init(context: Context?): Boolean {
            preferences = context?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            return true
        }

        override fun setData(key: String?, data: ByteArray?) {
            if (data == null) return
            val editor = preferences?.edit()
            editor?.putString(key, Base64.encodeToString(data, Base64.DEFAULT))
            editor?.apply()
        }

        override fun getData(key: String?): ByteArray? {
            val res = preferences?.getString(key, null) ?: return null
            return Base64.decode(res, Base64.DEFAULT)
        }

        override fun remove(key: String?) {
            val editor = preferences?.edit()
            editor?.remove(key)
            editor?.apply()
        }
    }


    private inner class PasswordStorageHelperSDK18 : PasswordStorageInterface {

        private val KEY_ALGORITHM_RSA: String = "RSA";

        private val KEYSTORE_PROVIDER_ANDROID_KEYSTORE: String = "AndroidKeyStore";
        private val RSA_ECB_PKCS1_PADDING: String = "RSA/ECB/PKCS1Padding";

        private var preferences: SharedPreferences? = null;
        private var alias: String? = null;

        @RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
        override fun init(context: Context?): Boolean {
            preferences = context?.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
            alias = context?.getString(R.string.app_package);

            val ks: KeyStore?

            try {
                ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);

                //Use null to load Keystore with default parameters.
                ks?.load(null);

                // Check if Private and Public already keys exists. If so we don't need to generate them again
                val privateKey: Key? = ks?.getKey(alias, null);
                if (privateKey != null && ks.getCertificate(alias) != null) {
                    val publicKey: PublicKey? = ks.getCertificate(alias).publicKey;
                    if (publicKey != null) {
                        // All keys are available.
                        return true;
                    }
                }
            } catch (ex: Exception) {
                return false;
            }

            // Create a start and end time, for the validity range of the key pair that's about to be
            // generated.
            val start = GregorianCalendar();
            val end = GregorianCalendar();
            end.add(Calendar.YEAR, 10);

            // Specify the parameters object which will be passed to KeyPairGenerator
            val spec: AlgorithmParameterSpec?
            if (Build.VERSION.SDK_INT < 23) {
                spec = context?.let {
                    android.security.KeyPairGeneratorSpec.Builder(it)
                            // Alias - is a key for your KeyPair, to obtain it from Keystore in future.
                            .setAlias(alias ?: "")
                            // The subject used for the self-signed certificate of the generated pair
                            .setSubject(X500Principal("CN=$alias"))
                            // The serial number used for the self-signed certificate of the generated pair.
                            .setSerialNumber(BigInteger.valueOf(1337))
                            // Date range of validity for the generated pair.
                            .setStartDate(start.time).setEndDate(end.time)
                            .build()
                };
            } else {
                spec = KeyGenParameterSpec.Builder(alias ?: "", KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
                        .build();
            }

            // Initialize a KeyPair generator using the the intended algorithm (in this example, RSA
            // and the KeyStore. This example uses the AndroidKeyStore.
            val kpGenerator: KeyPairGenerator
            try {
                kpGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
                kpGenerator.initialize(spec);
                // Generate private/public keys
                kpGenerator.generateKeyPair();
            } catch (e: Exception) {
                when (e) {
                    is NoSuchAlgorithmException, is InvalidAlgorithmParameterException, is NoSuchProviderException -> {
                        try {
                            ks?.deleteEntry(alias)
                        } catch (e1: Exception) {
                            // Just ignore any errors here
                        }
                    }
                }

            }

            // Check if device support Hardware-backed keystore
            try {
                var isHardwareBackedKeystoreSupported: Boolean? = null

                isHardwareBackedKeystoreSupported = if (Build.VERSION.SDK_INT < 23) {
                    KeyChain.isBoundKeyAlgorithm(KeyProperties.KEY_ALGORITHM_RSA)
                } else {
                    val privateKey: Key = ks.getKey(alias, null)
                    //KeyChain.isBoundKeyAlgorithm(KeyProperties.KEY_ALGORITHM_RSA)
                    val keyFactory: KeyFactory = KeyFactory.getInstance(privateKey.algorithm, "AndroidKeyStore")
                    val keyInfo: KeyInfo = keyFactory.getKeySpec(privateKey, KeyInfo::class.java)
                    keyInfo.isInsideSecureHardware
                }
                Log.d(tag, "Hardware-Backed Keystore Supported: $isHardwareBackedKeystoreSupported");
            } catch (e: Exception) {
                //KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | InvalidKeySpecException | NoSuchProviderException e
            }

            return true;
        }

        override fun setData(key: String?, data: ByteArray?) {
            var ks: KeyStore? = null;
            try {
                ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);

                ks.load(null);
                if (ks.getCertificate(alias) == null) return;

                val publicKey: PublicKey? = ks.getCertificate(alias).publicKey;

                if (publicKey == null) {
                    Log.d(tag, "Error: Public key was not found in Keystore");
                    return;
                }

                val value: String = encrypt(publicKey, data);

                val editor: SharedPreferences.Editor? = preferences?.edit();
                editor?.putString(key, value);
                editor?.apply();
            } catch (e: Exception) {
                when (e) {
                    is NoSuchAlgorithmException, is InvalidKeyException, is NoSuchPaddingException,
                    is IllegalBlockSizeException, is BadPaddingException, is NoSuchProviderException,
                    is InvalidKeySpecException, is KeyStoreException, is CertificateException, is IOException -> {

                        try {
                            ks?.deleteEntry(alias)
                        } catch (e1: Exception) {
                            // Just ignore any errors here
                        }
                    }
                }
            }
        }


        override fun getData(key: String?): ByteArray? {
            var ks: KeyStore? = null;
            try {
                ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
                ks.load(null);
                val privateKey: Key = ks.getKey(alias, null);
                return decrypt(privateKey, preferences?.getString(key, null));
            } catch (e: Exception) {
                //KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException
           

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...