Android密码存储实践

1、概述和思路
android应用跑在沙盒里,通常数据没有泄露风险。但是高手还是有办法获取应用生成的缓存、数据库等。如果将密码直接存储在数据库里面并不安全。即便是本地无关紧要的小应用最好也对存储的密码进行加密。
加密有很多方法,对称和非对称加密算法相比很多人都知道。AES是对称加密的代表,RSA是非对称加密算法的代表。在网络传输时,往往用RSA来加密AES的密钥传递给通讯方。tls通讯可以更深入了解一下。这里不再赘述。因为密码数据较少,使用RSA这种计算复杂度更高的加密算法对整体性能影响不是很大。
为了保证密码的安全,我们将用户名、用户组编号和密码用分隔符组合之后做一个字符串。然后将这个字符串存到byteArray里面进行加密。之后将用户名,用户组和加密后的这个字符串一起存在数据库中。
逻辑上似乎没有问题,但是我们到底怎么存储加密算法的密钥呐?
如果密钥以明文存储成文件或者存在一个非加密的数据库中,那整体上毫无意义。
读了官方的文档,它提供了两种方法(至少,如果有其它的请留言):EncryptedSharedPreferences和KeyStore两种方法。前者用系统的MasterKeys去加密SharedPreference存储的键值对。后者用TrustZone(需要硬件支持)等手段将密钥存在相对安全的区域。
有文章将KeyStore被删除,但具体机理没有搞明白。本来要用EncryptedSharedPreferences,却发现这个方法有几个问题。第一不容易写一个与ui无关的文件。无论MasterKey还是EncryptedSharedPreferences都需要context才可以。整个加密类的使用不太方便。放到viewModel里面不太好搞。另一个是EncryptedSharedPreferences存储密钥智能以String方式存储。需要将PublicKey和PrivateKey编码之后存在String里面用的时候再decode。非常麻烦,搞不好会出错。
多以最终选择了KeyStore的方式。

2、代码
话不多说,先上一段代码。这个是一个与ui无关的类。但是创建密钥和加解密这三个方法用suspend修饰。需要将其放在协程里面运行。更适合viewModel里面引用这个类。

package com.example.loginwithrsa.ui.navi


import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyPairGenerator
import java.security.KeyStore
import javax.crypto.Cipher


class PswSecurity() {

    private val cipherEncrypt = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256andMGF1Padding")
    private val cipherDecrypt = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256andMGF1Padding")
    private val charset = Charsets.UTF_8
    private val pswKeyAlias = "user_psw_key"
    private val provider = "AndroidKeyStore"
    private val keyStore = KeyStore.getInstance(provider).apply{
                                    try {
                                        load(null)
                                    }catch (e:Throwable){
                                        e.printStackTrace()
                                    }
                                }


    suspend fun createKeyPair(){
        /*try to find keystore*/
        try{
            if(keyStore == null || keyStore.containsAlias(pswKeyAlias)){
                val privateKeyEntry = keyStore.getEntry(pswKeyAlias, null) as KeyStore.PrivateKeyEntry
                val publicKey = privateKeyEntry.certificate.publicKey
                val privateKey = privateKeyEntry.privateKey
                cipherEncrypt.init(Cipher.ENCRYPT_MODE, publicKey)
                cipherDecrypt.init(Cipher.DECRYPT_MODE, privateKey)
            }else{
                val keyPairGenerator : KeyPairGenerator = KeyPairGenerator.getInstance(
                        KeyProperties.KEY_ALGORITHM_RSA,
                        provider)
                /*create*/
                val keyDecrypt = KeyGenParameterSpec.Builder(
                        pswKeyAlias,
                        KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .build()
                keyPairGenerator.initialize(keyDecrypt)
                val keyPair = keyPairGenerator.generateKeyPair()

                cipherEncrypt.init(Cipher.ENCRYPT_MODE, keyPair.public)
                cipherDecrypt.init(Cipher.DECRYPT_MODE, keyPair.private)
            }

        }catch (e: Throwable){
            e.printStackTrace()
            // throw e
        }
    }



    suspend fun encrypt(msg: String): String {
        return try{
            val iv = cipherEncrypt.iv
            val secret = cipherEncrypt.doFinal(msg.toByteArray(charset))
            Base64.encodeToString(secret, Base64.DEFAULT)
        }catch (e: Throwable){
            throw Throwable("can not encrypt", e)
        }
    }
    suspend fun decrypt(secret: String): String {
        return try {
            val msg = cipherDecrypt.doFinal(Base64.decode(secret, Base64.DEFAULT))
            msg.toString(charset)
            //String(msg)
        }catch (e: Throwable){
            throw Throwable("Can not decrypt", e)
        }
    }
}

3、讲解
上面在引用方法的时候需要先创建密钥对createKeyPair,创建ok之后才可以使用encrypt和decrypt实现加解密。
createKeyPair首先先检查是否存在pswKeyAlias ,如果存在表明这对密钥已经创建。用的时候直接使用就可以。否则就重新创建一对。KeyStore里面的这个Alias不知道能不能从其它应用里面读取。如果能够读取就非常不好。但是可以自己做一个加密的keyStore。这里暂时没有这么做。然后利用getEntry获取存储的入口。需要将其转换成KeyStore.PrivateKeyEntry。这样才可以从里面获取PublicKey和PrivateKey。(这一步刚开始没找到怎么搞,花了很多时间)
创建密钥官方有指导文档。就是用keyPairGenerator和KeyGenParameterSpec.Builder合作完成参数的设置。然后生成一对密钥。生成的密钥自动存到了keyStore里面。(之前一直没找到存到keyStore的方法。后来才理解。但是用户可以修改keyStore里面的值。)
加解密比较常规,使用Cipher。先getInstance设置算法。然后用Init初始化加密还是解密模式。用的时候用doFinal来实现。但是特别需要注意转码的过程。否则调试半天不知道哪里出错了。

4、参考文档或文章:
官方安全指南
官方keystore
Dson2020的Android KeyStore总结
github上一个哥们的gist

上一篇:基于frdia的安卓逆向工具hooker抓包soul app


下一篇:【已解决】解决高德地图key鉴权失败的问题。