Android 7.0后加密库Crypto被废弃后填坑

问题描述

Android中使用AES加密,在targetSdkVersion设置为25及以上后程序报错:

1
2
3
4
5
6
7
8
9
********** 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 文件。如果直接使用这些密钥是不会有任何问题的,但是有时候我们需要通过一个字符串格式的密码来生成密钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

解决方案主要来自上方的官方人员在异常日志中给出的链接(需要科学上网):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static byte[] getRawKeyCompat(String password) throws Exception {
// 密钥的比特位数,注意这里是比特位数
// AES 支持 128、192 和 256 比特长度的密钥
int keyLength = 256;
// 盐值的字节数组长度,注意这里是字节数组的长度
// 其长度值需要和最终输出的密钥字节数组长度一致
// 由于这里密钥的长度是 256 比特,则最终密钥将以 256/8 = 32 位长度的字节数组存在
// 所以盐值的字节数组长度也应该是 32
int saltLength = 32;

// 先获取一个随机的盐值
// 需要将此次生成的盐值保存到磁盘上下次再从字符串换算密钥时传入
// 如果盐值不一致将导致换算的密钥值不同
// 保存密钥的逻辑官方并没写,需要自行实现
// SecureRandom random = new SecureRandom();
// byte[] salt = new byte[saltLength];
// random.nextBytes(salt);
byte[] salt = Crypto.getSalt().getBytes();

// 将密码明文、盐值等使用新的方法换算密钥
int iterationCount = 1000;
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength);
SecretKeyFactory keyFactory = SecretKeyFactory
.getInstance("PBKDF2WithHmacSHA1");
// 到这里就能拿到一个安全的密钥
byte[] raw = keyFactory.generateSecret(keySpec).getEncoded();
return raw;
}

以上就是正确从字符串中获取密钥的方法。官方还十分贴心的提供了一个例子,其中包含一个可以用于辅助解密由被废弃的逻辑加密出来的数据的工具类,有需要的朋友可自行拿取(需要科学上网):

Samples:BrokenKeyDerivation

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :