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