安全加解密

"你好,未来"

Posted by hbl on 2017-02-12

方案确定

常见的加密方式有两大类分为对称加密和非对称加密两种,其中对称加密采用的是AES的加密方式,非对称的加密采用的是RSA,考虑到非对称加密在加密大量数据时会比较影响性能,所以具体的方案是使用AES对数据源进行加密,之后使用RSA将AES加密使用的密钥进行加密,给后端传递的数据就是使用AES加密后的密文和使用RSA加密的AES密钥的密文。

具体加密方式的确定

RSA

Android端在使用RSA加密采用的是之前已经有的一套RSA加密算法,主要方法包括获取公钥,使用公钥加密,这两个方法,其中比较重要就是在使用加密算法的时候通常要确定所使用算法的工作模式和填充方式,使用公钥和算法,模式不同也是无法解析的,通过与后端联调之后确定RSA的填充模式为RSA/None/PKCS1Padding ,具体方法如下:

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
29
30
31
32
// 通过公钥byte[]将公钥还原,适用于RSA算法
public static PublicKey getPublicKey(byte[] keyBytes) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
* @throws Exception
*/
public static String encryptByPublicKey(String data, RSAPublicKey publicKey)
throws Exception {
Cipher cipher = Cipher.getInstance(KEY_PAIRGENO);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 模长
int key_len = publicKey.getModulus().bitLength() / 8;
// 加密数据长度 <= 模长-11
String[] datas = splitString(data, key_len - 11);
String mi = "";
// 如果明文长度大于模长-11则要分组加密
for (String s : datas) {
mi += new String(UrlBase64.encode(cipher.doFinal(s.getBytes())));
}
return mi;
}

AES

在选择AES加密的具体实现方式的时候经历了许多波折,现在看来主要问题集中在这两方面

  1. 如何才能生成一个真正安全随机的密钥
  2. 怎么使用生成的密钥进行加密

先说第一种,如果才能生成一个安全随机密钥,最初使用的方案方式是使用传统的setSeed()方法

1
2
3
4
5
6
7
8
9
private static SecretKeySpec getSecretKeySpec(String key) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(key.getBytes());
keyGen.init(128,random);
SecretKey secretKey = keyGen.generateKey();
byte[] encoded = secretKey.getEncoded();
return new SecretKeySpec(encoded, "AES");
}

使用这种方式进行加密之后发现后端解析不了,排查发现是因为虽然算法选择的都是同样的 SHA1PRNG,但是默认的算法提供程序不一样,查看前端和后端各有哪些算法提供程序,看看有没有相同的,结果如下:

上侧绿色的是模拟器的,下侧是后端发给我的,可以明显的看出来,完全没有一个一样的,并且通过调查发现在Android的不同系统版本上,默认的算法提供程序是不一样的,4.2以下的使用的是Crypto,4.2以上使用的是OpenSSL,这就导致我们在选择算法提供程序的时候需要根据版本号去进行区分,网络上的建议是统一使用Crypto,向下兼容,不过这就会衍生出新的问题,在最新的Android N的源码中可以找到这样的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ("Crypto".equals(provider)) {
System.logE(" ********** PLEASE READ ************ ");
System.logE(" * ");
System.logE(" * New versions of the Android SDK no longer support the Crypto provider.");
System.logE(" * If your app was relying on setSeed() to derive keys from strings, you");
System.logE(" * should switch to using SecretKeySpec to load raw key bytes directly OR");
System.logE(" * use a real key derivation function (KDF). See advice here : ");
System.logE(" * http://android-developers.blogspot.com/2016/06/security-crypto-provider-deprecated-in.html ");
System.logE(" *********************************** ");
if (VMRuntime.getRuntime().getTargetSdkVersion()
<= sdkTargetForCryptoProviderWorkaround) {
System.logE(" Returning an instance of SecureRandom from the Crypto provider");
System.logE(" as a temporary measure so that the apps targeting earlier SDKs");
System.logE(" keep working. Please do not rely on the presence of the Crypto");
System.logE(" provider in the codebase, as our plan is to delete it");
System.logE(" completely in the future.");
return getInstanceFromCryptoProvider(algorithm);
}
}

大致的意思就是使用Crypto这种算法提供程序在新的版本中不被支持了,同样SHA1PRNG也将要被删除,推荐使用SecretKeySpec这种方式,根据提供的文档和查询的相关资料,找到了新的安全随机密钥生成方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*User types in their password: */
String password = "password";
/* Store these things on disk used to derive key later: */
int iterationCount = 1000;
int saltLength = 32; // bytes; should be the same size as the
output (256 / 8 = 32)
int keyLength = 256; // 256-bits for AES-256, 128-bits for AES-128, etc
byte[] salt; // Should be of saltLength
/* When first creating the key, obtain a salt with this: */
SecureRandom random = new SecureRandom();
byte[] salt = new byte[saltLength];
random.nextBytes(salt);
/* Use this to derive the key from the password: */
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");

新的安全随机密钥生成方式与之前的生成方式最大的区别是密钥的生成算法的改变,从”SHA1PRNG“变成”PBKDF2WithHmacSHA1 “,之后就和后端和IOS三端讨论,因为IOS在使用AES进行加密的的时候只能使用AES/ECB/PKCS7Padding 填充模式,java本身是不支持的,所以引入一个第三方的算法提供程序(bouncycastle),新的算法提供程序同样也是使用该jar包中提供的BC算法,使用BC的安全随机密钥生成方式:

1
2
3
4
5
6
7
8
9
10
11
private static SecretKeySpec getSecretKeySpec(String key) throws Exception {
//实例化密钥生成器
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
KeyGenerator kg= KeyGenerator.getInstance(KEY_ALGORITHM, "BC");
//初始化密钥生成器,AES要求密钥长度为128位、192位、256位
kg.init(128);
//生成密钥
SecretKey secretKey=kg.generateKey();
byte[] encoded = secretKey.getEncoded();
return new SecretKeySpec(encoded, "AES");
}

这样安卓端密钥的生成方式就确定下来了,采用最新的密钥生成方式,以BC算法提供程序为依靠,使用 “AES/ECB/PKCS7Padding“ 进行加密,多方整合之后,属于我们自己的AES算法就诞生了,贴一下主要函数:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 获得密钥的字符串形式
*
* @param password
* @return
* @throws UnsupportedEncodingException
*/
public static String getHexKey(String password) throws UnsupportedEncodingException {
byte[] b = new byte[16];
SecureRandom random=new SecureRandom();
random.nextBytes(b);
byte[] salt = b;
SecretKey key ;
try {
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_LENGTH);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(PBKDF2_DERIVATION_ALGORITHM);
byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
key= new SecretKeySpec(keyBytes, "AES");
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = key.getEncoded();
return toHex(keyBytes);
}
/**
* 加密
*
* @param data 被加密的数据
* @param password 密钥
* @return
*/
public static String encrypt(String data, String password) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
byte[] keyBytes = password.getBytes();
byte[] result = null;
try {
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM, "BC");
cipher.init(Cipher.ENCRYPT_MODE, key);
result = cipher.doFinal(data.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
return new String(UrlBase64.encode(result));
}

这个就是目前使用的加密方案,从密钥的生成到数据的加密,关键的技术点在之前已经说过了,现在说一下在整合过程中出现的问题及解决方案:

  1. 原始密钥,使用简单随机数生成。
  2. Base64的选择,统一选择引入的BC包中的UrlBase64。

网络传递

Android中网络请求使用的是OKhttp,利用其拦截器的机制,可以在请求的Request和Response进行统一更改,具体的更改逻辑如下:

加密:

  1. 根据随机生成的字符串生成AES的密钥,并使用RSA对生成的密钥进行加密。
  2. 判断请求类型,GET请求则截取”?”后的字符串,将其变成Json类型并进行AES加密,POST请求则判断请求类型,加密POST的body中数据。
  3. 将加密好的密文和AES的密钥根据不同的数据类型拼接成指定格式。
  4. 在加密成功的网络请求地址后面拼接加密标识。

解密

  1. 判断返回的数据类型,进行过滤。
  2. 取出body中内容,获得AES解密的密钥,与本地密钥进行拼接。
  3. 使用拼接后的密钥对数据进行解密。

注意问题

  1. 对POST请求类型进行过滤。
  2. 请求时,将POST请求后的Content_Type改为Json类型

使用方式

  1. 添加远程依赖
  2. 在初始化构建OkHttpClient的时候添加加解密拦截器即可。