Android 安全存储——Android Keystore

KeyStore 其实并不是 Android 独有,它是 JCA(Java 密码学架构)里的密钥库门面类,实现由 Provider 通过 KeyStoreSpi 提供。在 Android 上,系统注册了一个名为 AndroidKeyStore 的 Provider,把 KeyStoreSpi 接到了硬件密钥库上。它支持 AndroidKeyStore、AndroidCAStore、BKS、PKCS12 这几种 type。

开局常量配置

可以声明配置密钥别名、指定加密算法组合和初始化向量的长度(IV)。

  • 密钥名要具有唯一性,最好带个版本号,方便以后处理些东西。
1
2
3
private const val KEY_ALIAS = "lime_token_key_v1"
private const val TRANSFORMATION = "AES/GCM/NoPadding"// 算法字符串
private const val IV_LENGTH = 12// GCM 规定的标准 IV 长度

算法字符串是要按照 算法 / 模式 / 填充 的格式来配置,通常是 "AES/GCM/NoPadding",AES 是一种对称加密算法,GCM 是工作模式,NoPadding 是无填充。GCM 是一种认证加密模式,不仅可以保证数据的机密性,还能保证数据的完整性(也就是防篡改)。

IV 的作用是用同样的明文和同一把密钥,让每次加密出来的密文都不一样。

打开密钥库并生成密钥

向系统索要一个密钥库对象

需要先指定密钥库类型,这里选 "AndroidKeyStore" 就行,数据由系统硬件保管,不用密码,所以适合用来加密本地数据、存令牌。但这时它还没有和系统的密钥服务连上,硬件密钥库又不需要从文件读数据、也不需要密码,所以打开密钥库时参数传 null 即可。

1
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }

在密钥库里生成密钥

要一个密钥生成器,填上你要的算法和提供者。

1
generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")

用 KeyGenParameterSpec 说明用途和规格,

1
2
3
4
5
6
7
8
9
10
generator.init(
KeyGenParameterSpec.Builder(
KEY_ALIAS, // 密钥名
PURPOSE_ENCRYPT or PURPOSE_DECRYPT// 用途:允许加密和解密
)
.setBlockModes(BLOCK_MODE_GCM)// 加密模式
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)// 填充操作
.setKeySize(256)// 密钥长度,默认 128
.build()
)

生成 key 引用

1
return generator.generateKey()

附一张表供自查:

Provider(提供者) 它提供的 KeyStore 类型
AndroidKeyStore AndroidKeyStore
AndroidOpenSSL(Conscrypt) AndroidCAStore
BC(BouncyCastle) BKS、PKCS12(还有 BouncyCastle 等别名)

加密

创建 Cipher,获取加密工具。

1
val cipher = Cipher.getInstance(TRANSFORMATION)// 算法 / 模式 / 填充

初始化,指定密钥与选择加密操作。

1
cipher.init(Cipher.ENCRYPT_MODE, SecretKey)// 加密这把密钥

取出 IV ,并执行加密。

对了,还要注意 IV 是在 init() 那一刻由系统生成的,切记要在 doFinal 前取出来,不然的话加密结束 Cipher 对象的状态会被重置。

1
2
val iv = cipher.iv
val encrypted = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))

最后是组装,把 IV 和密文拼接,再转成 Base64 字符串

1
Base64.encodeToString(iv + encrypted, Base64.NO_WRAP)

这样加密之后就是这个形式啦。

Base64( [IV 12 字节] [密文] [校验标签 16 字节] )

解密

解密当然是加密的逆操作,和加密时反着来。

把 Base64 字符串还原成明文,把存进去的字符串变回字节数组。

1
val raw = Base64.decode(encoded, Base64.NO_WRAP)

解铃还须系铃人,按约定的格式拆开后,再有请 Cipher 😄

1
2
3
val iv = raw.copyOfRange(0, IV_LENGTH)
val body = raw.copyOfRange(IV_LENGTH, raw.size)
val cipher = Cipher.getInstance(TRANSFORMATION)

解密时要用原来的 IV ,需手动传,用 GCMParameterSpec 装 GCM 加解密需要的两个参数 。即校验标签长度和 IV。

1
cipher.init(Cipher.DECRYPT_MODE, SecretKey, GCMParameterSpec(128, iv))

没啥可说的,依旧逆操作,然后你就解密成功了!

1
String(cipher.doFinal(body), Charsets.UTF_8)

附

此由 AI 生成 👇,未经核实研究,请辩证看待。

场景 用什么 密钥存哪
加密本地小数据(token、配置、缓存)——你这个项目 AES/GCM/NoPadding AndroidKeyStore(硬件,不可导出)
加密大文件 同样 AES/GCM,但走流式或分段 AndroidKeyStore
用户设了密码,要用密码加密数据 AES/GCM + PBKDF2 从密码派生密钥 不需要密钥库,密钥每次从密码算出来
要跨设备/跨平台解密(换手机还能解、后端也能解) AES/GCM 不能用 AndroidKeyStore,密钥必须能导出或由密码派生
和后端交换加密数据 AES/GCM 加密数据 + RSA/OAEP(或 ECDH)传密钥 后端公钥 + 本地随机密钥
网络传输 不要自己加密 交给 HTTPS/TLS
存密码(要能验证,不能还原) PBKDF2WithHmacSHA256 + 随机 salt 不适用(不可逆)
校验数据有没有被改(不保密) HmacSHA256(需要防伪造)或 SHA-256(只防意外损坏) HMAC 的密钥放 AndroidKeyStore
签名/身份验证 Signature.getInstance("SHA256withECDSA") 或 SHA256withRSA AndroidKeyStore(KeyPairGenerator)
必须解锁手机才能用 同上 + setUserAuthenticationRequired(true) AndroidKeyStore