iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Mobile Development

花30天做個Android小專案系列 第 29

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 儲存敏感性資料


上一篇
Day28 - 儲存帳密及自動登入
下一篇
Day30 - 第一次鐵人賽心得
系列文
花30天做個Android小專案30

尚未有邦友留言

立即登入留言