Day29 - 使用Keystore加密密码

今天要来处理储存密码的安全问题。

话是这麽说,但要明白即使我们将密码加密储存了,但在使用过程中依旧会有暴露的风险。因此还是会建议在使用这个App的时候不要使用本尊帐号(这也是我没做推文功能的原因之一),并且定期更换密码,除此之外平时手机的使用习惯还是要谨慎为上!

Android Keystore System

Android keystore system是Android中用来储存加密金钥的系统,自Android 7.0起,在Android Compatibility Definition Document中已将几种演算法的金钥储存至Hardware Backed Keystore订为MUST have

MUST have hardware backed implementations of RSA, AES, ECDSA and HMAC cryptographic algorithms and MD5, SHA1, SHA-2 Family hash functions to properly support the Android Keystore system's supported algorithms.

以上节录自Android 7.0 CDD - Keys and Credentials

因此以目前来说使用Keystore System来储存我们的金钥是相对安全的做法。

演算法选择

以我们的使用情境来说,需要加密的资料只有密码,因此资料量不大。在这情况下我是选择以安全性为主,因此直接使用RSA演算法做加解密。

产生RSA金钥

以下产生金钥方法为API 23以上的做法,兼容低版本的部分可参考文末的参考文章。

class KeystoreUtil {
    private val keyStoreProvider = "AndroidKeyStore"
    private val alias = "ALIAS_CA"

    private val keystore: KeyStore = KeyStore.getInstance(keyStoreProvider)

    init {
        keystore.load(null)
        if (!keystore.containsAlias(alias)) {
            genRSAKey()
        }
    }

    private fun genRSAKey() {
        val keyPairGenerator =
            KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, keyStoreProvider)
        val keyGenParameterSpec = KeyGenParameterSpec
            .Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
            .build()

        keyPairGenerator.initialize(keyGenParameterSpec)
        keyPairGenerator.generateKeyPair()
    }
    // ...
}

加密字串

class KeystoreUtil {
    // ...
    public fun encrypt(plaintext: String): String {
        val publicKey = keystore.getCertificate(alias).publicKey

        val cipher = Cipher.getInstance(rsaMode)
        cipher.init(Cipher.ENCRYPT_MODE, publicKey)

        return cipher.doFinal(plaintext.toByteArray()).toHexString()
    }
    // ...
}

toHexString是另外做的扩展函数,将加密後的ByteArray转为Hex字串:

fun ByteArray.toHexString(): String =
    joinToString(separator = "") { byte ->
        "%02x".format(byte)
    }

解密字串

class KeystoreUtil {
    // ...
    public fun decrypt(encryptedText: String): String {
        val privateKey = keystore.getKey(alias, null) as PrivateKey

        val cipher = Cipher.getInstance(rsaMode)
        cipher.init(Cipher.DECRYPT_MODE, privateKey)

        return cipher.doFinal(encryptedText.hexToByteArray()).toString(StandardCharsets.UTF_8)
    }
    // ...
}

hexToByteArray则是对应的将Hex字串转回ByteArray的方法:

fun String.hexToByteArray(): ByteArray =
    chunked(2)
        .map { it.toInt(16).toByte() }
        .toByteArray()

另外注意解密回来後的ByteArray在转回String时有带入StandardCharsets.UTF_8,这是因为Kotlin String预设的toByteArray中有预设指定的Charset Type。

public inline fun String.toByteArray(charset: Charset = Charsets.UTF_8): ByteArray = 
    (this as java.lang.String).getBytes(charset)

修改LoginFragment

KeystoreUtil完成後就可以直接把昨天存取密码的位置做更换了。

class LoginFragment : Fragment() {
    // ...
    private val keystoreUtil = KeystoreUtil()
    // ...
}

储存至SharedPreferences

// ...
if (binding.savePwd.isChecked) {
    editor.putString(PREF_FIELD_PWD, keystoreUtil.encrypt(pwd))
}
// ...

提取自SharedPreferences

// ...
val encryptedPwd = preferences.getString(PREF_FIELD_PWD, null)
if (!encryptedPwd.isNullOrBlank()) {
    binding.pwdInput.setText(keystoreUtil.decrypt(encryptedPwd))
    binding.savePwd.isChecked = true
}
// ...

加密结果

https://ithelp.ithome.com.tw/upload/images/20211013/201246027gXyizDsGP.png

参考文章

以下文章使用非对称式演算法加密金钥对称式演算法加密内文的方法是较为兼顾效能与安全性的方法。
使用Android KeyStore 储存敏感性资料


<<:  虹语岚访仲夏夜-30(打杂的Allen终)

>>:  [Python 爬虫这样学,一定是大拇指拉!] DAY29 - 实战演练:自制进度条 Progress Bar

Day09 | Dart 非同步 - Future

昨天介绍了在Dart中非同步的基本概念,今天就要来讲到如何简单的控制非同步操作。 Future Fu...

Google Maps JavaScript API 工具|专案实作

串接地图 JavaScript API 中虽然相较起来难度较高,不过官方文件写的也很简单易懂。 使用...

[Android Studio] -- Day 4 Gallery与EcoGallery

前言 过年爽爽放,该回来复习复习拉WW,大家新年快乐 今天原本是要来练习以前都没接触过的galler...

【Day 05】LeetCode:Plus One ( 用 JavaScript 学演算法 )

我们继续透过 LeetCode #66 Plus One 来实际感受解决问题的过程 ( 题目连结 )...

DE2_115(DAY2)用niosii和switch还有NiosII console去控制板子上的led

DE2_115(DAY2)用niosii和switch还有NiosII console去控制板子上的...