【转载请注明出处】
笔者:DrkCore (http://blog.csdn.net/DrkCore)
原文链接:(http://blog.csdn.net/drkcore/article/details/69931654)
一、 问题描述
笔者偶尔会写一些自己的小应用,其中有个功能就是九宫格应用锁。通常情况下我们是用数字来标志九宫格的解锁顺序,并将该顺序组成字符串加密了放在 SharedPreferences 中的。一切都很正常,直到笔者将项目的 targetSdkVersion 设置为了 25 后程序崩溃了:
********** PLEASE READ ************
*
* New versions of the Android SDK no longer support the Crypto provider.
* If your app was relying on setSeed() to derive keys from strings, you
* should switch to using SecretKeySpec to load raw key bytes directly OR
* use a real key derivation function (KDF). See advice here :
*
http://android-developers.blogspot.com/2016/06/security-crypto-provider-deprecated-in.html
***********************************
我们知道加密算法都是需要密钥的,比如 AES 算法支持128 比特、192 比特和 256 比特三种长度的密钥,通常这些密钥会被转化成字节数组明文写在代码中或者写入成 KeyStore 文件。如果你是直接使用这些密钥的话是不会有任何问题的,但是有的时候我们需要通过一个字符串格式的密码来生成密钥。
按照搜索引擎上找来的方法你可能会写出如下的代码,然后遇到和笔者一样的问题:
public static final String DEPREACATED_SECURE_ALGORITHM_SHA1PRNG = "SHA1PRNG";
public static final String DEPREACATED_SECURE_PROVIDER_CRYPTO = "Crypto";
/**
* 按照指定编码从字符串中生成指定长度的密钥 key。
*
* @param password
* @param charset
* @param keyBitLen
* @return
* @throws NoSuchProviderException
* @throws NoSuchAlgorithmException
*/
@Deprecated
public static byte[] deriveKeyDeprecated(String password, @Nullable Charset charset, int keyBitLen) throws NoSuchProviderException, NoSuchAlgorithmException {
SecureRandom secureRandom = SecureRandom.getInstance(DEPREACATED_SECURE_ALGORITHM_SHA1PRNG, DEPREACATED_SECURE_PROVIDER_CRYPTO);
//在随机数生成器中将密码的字符串设为种子换算出最终的密钥key,异常就是在这里发生的
secureRandom.setSeed(password.getBytes(charset != null ? charset : Charset.defaultCharset()));
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES);
keyGenerator.init(keyBitLen, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
return secretKey.getEncoded();
}
可以看到我们将密码作为随机数生成器的种子换算出密钥 key,这种做法已经被认定为是不安全的。官方开发人员在 Axndroid N 上已经将相关的 Crypto provider 和 SHA1PRNG 算法同时废弃掉了,并计划在后续的 SDK 中完全移除相关的库。
当然你可以直接使用密钥来绕过这个问题,或者将 targetSdkVersion 调低一些来掩盖崩溃,但这个坑早晚总是要填的。所以让我们来看看该怎么解决这个问题。
二、 解决方案
Android-Developers-Blogspot: security-crypto-provider-deprecated-in
解决方案主要来自上方的官方人员在异常日志中给出的链接(需要科学上网),笔者主要是将核心的逻辑和说明整理出来而已。以下是解决这个问题的核心代码:
// 给出字符串的密码
String password = "password";
// 密钥的比特位数,注意这里是比特位数
// AES 支持 128、192 和 256 比特长度的密钥
int keyLength = 256;
// 盐值的字节数组长度,注意这里是字节数组的长度
// 其长度值需要和最终输出的密钥字节数组长度一致
// 由于这里密钥的长度是 256 比特,则最终密钥将以 256/8 = 32 位长度的字节数组存在
// 所以盐值的字节数组长度也应该是 32
int saltLength = 32;
byte[] salt;
// 先获取一个随机的盐值
// 你需要将此次生成的盐值保存到磁盘上下次再从字符串换算密钥时传入
// 如果盐值不一致将导致换算的密钥值不同
// 保存密钥的逻辑官方并没写,需要自行实现
SecureRandom random = new SecureRandom();
byte[] salt = new byte[saltLength];
random.nextBytes(salt);
// 将密码明文、盐值等使用新的方法换算密钥
int iterationCount = 1000;
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt,
iterationCount, keyLength);
SecretKeyFactory keyFactory = SecretKeyFactory
.getInstance("PBKDF2WithHmacSHA1");
// 到这里你就能拿到一个安全的密钥了
byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
SecretKey key = new SecretKeySpec(keyBytes, "AES");
以上就是正确地从字符串中获取密钥的方法。官方还十分贴心地提供了一个例子,其中包含了一个可以用于辅助解密由被废弃的逻辑加密出来的数据的工具类,有需要的朋友可自行拿取(需要科学上网):
当然你也可以看看笔者是如何从这个坑里面爬出来的,也可以参见我的 GitHub 项目中的部分源代码: