AndroidKeystore密钥库系统,保证本地加密算法的密钥安全性
1、AndroidKeystore密钥库系统介绍
AndroidKeystore
系统是一个密钥库管理系统,谷歌设计这个系统的初衷应该是为了对标苹果的钥匙串KeyChain
,有意思的是谷歌在Android4.0(API14)时便引入了KeyChain
,但是并未提供详尽的说明文档,仅仅提供了一个API文档,网上也没有什么对它的文章,着实让人费解。话不多说,有兴趣的同学可以自行搜索,下面我来给大家介绍一下今天的主角——AndroidKeystore
。
谷歌在Android4.3(API18)中引入了AndroidKeystore
,在Android6.0(API23)中对AndroidKeystore
进行了一波升级优化,目前来说应该是最安全的加解密方案。根据官方文档说明,原文对它的介绍如下:
利用 Android Keystore 系统,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入 Keystore 后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,它提供了密钥使用的时间和方式限制措施,例如要求进行用户身份验证才能使用密钥,或者限制为只能在某些加密模式中使用。
乍一看,这似乎是一个密钥保险箱,安全性高,可以把加密密钥存入其中,需要的时候再通过它用我们的密钥。可是!!这货的使用根本就不是个存储空间!!我们先来看看这东西大致的使用流程是怎样的,让大家直观的明白它的作用:
// step 1 通过密钥别名判断是否已有密钥
keyStore.containsAlias(keyAlias);
// step 2 没有密钥,生成一个
spec = new KeyGenParameterSpec.Builder(keyAlias, ...)...;
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
// step 3 取出公钥,进行加密
publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
cipher.doFinal(data);
// step 4 取出私钥,进行解密
privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
cipher.init(Cipher.DECRYPT_MODE, privateKey);
cipher.doFinal(data);
如此看来,AndroidKeystore
根本就不是个密钥存储库。这和EncryptUtil这样的工具类几乎是一样的,先init,再encrypt,然后decrypt,俨然一个加解密工具。说白了,它在内部生成一个密钥,我们可以用它来加解密数据,并不能把我们自己生成的密钥送进去。
根据文档的介绍,它的大部分功能都只在Android6.0(API23)及以上支持,最低支持的Android4.3(API18)只提供RSA/ECB算法的使用。
2、密钥的检测与生成
AndroidKeystore是通过密钥的别名来进行加解密的,那么我们在使用之前必然是需要判断一下密钥是否存在的,AndroidKeystore提供了containsAlias()方法来完成密钥的检测,具体使用如下:
public boolean hasKey(String keyAlias) {
try {
// 加载一个AndroidKeyStore类型的KeyStore,貌似是定死的类型。
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
return keyStore.containsAlias(keyAlias);
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
这里可以"AndroidKeyStore"用常量存一下,后面还会用到,避免魔法字符串。关于字符串常量我就不单独提出来了,不方便阅读,接下来有使用的字符串都建议在项目中提取为字符串常量。
密钥的检测很简单,我就不赘述了,这里不得不吐槽一句,异常真多,当然后面也是一样的多异常,总让人用起来不踏实的感觉。
如果密钥库中没有该别名的密钥,那么我们得让它生成一个,重头戏来了,敲黑板!!!
public void buildKey(Context context, String keyAlias) {
try{
// 先获取密钥对生成器,采用RSA算法,AndroidKeyStore类型
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
// 加密算法的相关参数
AlgorithmParameterSpec spec;
// 密钥的有效起止时间,从现在到999年后,时间大家自己定
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 999)
// 生成加密参数,从Android6.0(API23)开始有所不同
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 根据密钥别名生成加密参数,提供加密和解密操作
spec = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
// SHA (Secure Hash Algorithm,译作安全散列算法) 是美国国家安全局 (NSA) 设计,美国国家标准与技术研究院 (NIST) 发布的一系列密码散列函数。 感兴趣的同学可以了解一下
.setDigests(KeyProperties.DIGEST_SHA512)
// 填充模式,一般RSA加密常用PKCS1Padding模式
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
// 限定密钥有效期起止时间
.setCertificateNotBefore(start.getTime())
.setCertificateNotAfter(end.getTime())
.build();
} else {
// 相对于Android6.0(API23)的方式,这种稍显简单
spec = new KeyPairGeneratorSpec.Builder(context.getApplicationContext())
.setAlias(keyAlias)
// 设置用于生成的密钥对的自签名证书的主题,X500Principal这东西不认识,资料真少,看的头大
.setSubject(new X500Principal("CN=" + keyAlias))
// 设置用于生成的密钥对的自签名证书的序列号,从BigInteger取即可
.setSerialNumber(BigInteger.TEN)
// 限定密钥有效期起止时间
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build()
}
// 用加密参数初始化密钥对生成器,生成密钥对
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
}
话不多说,都在代码注释里了,有没讲清楚的地方,欢迎评论区指出。
3、加解密流程
来吧,进入令人颤抖的加解密流程,真的,当我写完我都不可思议。
加解密的流程非常相似,先加载KeyStore,再拿到公钥/私钥,最后进行加密/解密。代码实现也基本一致,那么问题来了,令人颤抖的是什么呢?亮代码:
// 加密方法
public byte[] encrypt(Context context, String keyAlias, byte[] data) {
if(!hasKey(keyAlias)) {
buildKey(context, keyAlias);
}
try {
// 获取"AndroidKeyStore"类型的KeyStore,加载
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// 拿到密钥别名对应的Entry
KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
if (entry instanceof KeyStore.PrivateKeyEntry) {
// 通过Entry拿到公钥对象(并不是真实的公钥,仅供加密方法使用)
PublicKey publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
// 使用"RSA/ECB/PKCS1Padding"模式进行加密
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnrecoverableEntryException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return null;
}
// 解密方法
public byte[] decrypt(Context context, String keyAlias, byte[] data) {
if(!hasKey(keyAlias)) {
buildKey(context, keyAlias);
}
try {
// 获取"AndroidKeyStore"类型的KeyStore,加载
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// 拿到密钥别名对应的Entry
KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
if (entry instanceof KeyStore.PrivateKeyEntry) {
// 通过Entry拿到私钥对象(并不是真实的私钥,仅供解密方法使用)
PrivateKey privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
// 使用"RSA/ECB/PKCS1Padding"模式进行解密
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnrecoverableEntryException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return null;
}
谜题揭晓,令人颤抖的异常!!这是我至今为止,捕获的最多的异常了,果真是头发短见识短,疫情让我的头发长长了,见识的增长也接踵而来。
4、常规用法
通常只把AndroidKeystore用于对密钥的加密,也就是说,建议将需要加密的数据使用对称加密算法(如AES)进行加密,而对称加密算法的密钥则由AndroidKeystore进行加密保护。通常用法如下:
// saveKey()方法用于将加密后的aesKey持久化,parseByte2Hex()方法用于将二进制转为十六进制,防止byte[]转String导致格式出错的问题。
saveKey(parseByte2Hex(encrypt(context, keyAlias, aesKey)));
// readKey()方法用于读取持久化的加密的aesKey,parseHex2Byte()方法用于将十六进制转为二进制,防止String转byte[]导致格式出错的问题。
aesKey = decrypt(context, key, parseHex2Byte(readKey()));