需求: 与对接方对接一个接口,接口需使用固定种子,AES-128算法生成秘钥,并对报文信息进行加密问题表现: 公司自有运行环境与项目运维运行环境,接口都可以正常加解密调通接口,但生产环境调用接口对接方会解密失败,且报文内容通过自己写的解密方法也无法解密排查流程: 1、查看生产环境应用日志,定位问题 对接方返回结果只显示解密失败,我们自己的日志只打印了加密结果,于是把测试环境的接口地址,算法种子等参数都配置成生产相同,得到的打印结果(即加密后的数据不相同) 2、打印加密算法固定seed,以及AES-128使用该种子生产的秘钥的二进制信息(因为秘钥是byte数据,打二进制会方便查看些)3、通过前面两个步骤,最终定位到问题是相同的seed,在生产与测试环境下生成的秘钥不一致,导致最终加密的结果不相同,对接方无法解密
// 代码中生成秘钥的方法如下
public static byte[] generateAesKey(String key) {
byte[] keys = null;
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
keys = secretKey.getEncoded();
} catch (GeneralSecurityException e) {
logger.error(e.getMessage(), e);
}
return keys;
}
4、然后就是苦逼的上网查看大量资料,博客,网上提到方法,主要是两种1)使用第三方加密提供者BouncyCastleProvider,设置算法提供者为BC 即项目引入BouncyCastleProvider的jar包,代码动态增加入该算法提供者,使用算法时指定算法提供者 项目是maven项目,所以增加依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.59</version>
<scope>compile</scope>
</dependency>
///修改内容
static {
Security.addProvider(new BouncyCastleProvider());
}
///
public static byte[] generateAesKey(String key) {
byte[] keys = null;
try {
///修改内容
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES","BC");
///
//防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
keys = secretKey.getEncoded();
} catch (GeneralSecurityException e) {
logger.error(e.getMessage(), e);
}
return keys;
}
结果 使用该方法升级了一版,但经测试还是老问题,修改算法提供者并没有解决问题
然后又是查资料查资料…
终于在其它人的博客中有提到,SecureRandom的SHA1PRNG算法会因为运行环境不同而有差异,于是火速去查了下生产和两个测试环境运行环境以及对接方运行环境,如下
生产:winServer 部署WAS 使用IBM自带JDK 版本1.6
测试1:winServer 部署WAS 使用IBM自带JDK 版本1.8
测试2:winServer 部署Tomcat 使用SUN JDK 1.6
对接方:Linux SUN JDK 1.8
经网上查看资料(JCE,SecureRandom,IBMSecureRandom相关资料),资料中有提到,jdk静态配置的算法提供者列表信息,放在
%JAVA_HOME%\jre\lib\security\java.security中,于是取出正式与测试环境该文件查看,在jdk1.8.0_121的对应java.security中有列出算法提供者的列表,如图:
上述图片内的大致意思是,算法提供者的列表默认格式为:security.provider.=
其中n为优先级,n的数字越小越优先,且n的配置需要顺序配置,不可以空数字(比如有security.provider.1,security.provider.3但是没有security.provider.2),顺序可以打乱但不可以空下面一段就主要是讲SecureRandom的种子来源
上图主要是说到SecureRandom的种子来源配置,linux,unix,windows系统配置不太相同,具体可以参照自己项目使用的jdk的java.security文件来配置
这项配置也可以通过配置jvm参数来配置,等同于-Djava.security.egd=file:/dev/random,
这个配置不一致的话也可能会导致生成秘钥不一致,不过涉及JDK运行配置,不到必要不要去改
这段里面说明了SecureRandom的增强算法,来源为 SUN
关于JCE,以及相关知识,文末会列出写本记录参考到所有博客资料信息,因为自己写的比较粗糙,只作为问题记录
这块的话主要是写对于SecureRandom的一些了解,楼主是去取了生产和测试的的java.security配置大致都看了一遍,一般生产运行环境是不会轻易让改动这些的,所以网上提供的第二种办法2)修改java.security,静态的增加算法提供者,jdk的扩展中增加对应包 即增加算法提供者列表,将自己需要的提供者优先级提高,并增加对应提供者的jar包到jdk的配置中,这个做法的话,有一定的风险的影响项目上已写过的算法的应用,因有风险方案被拒
**
最后就是楼主具体解决问题的方法了
**
在了解了前面这些知识点的前提下,搜索IBM和sun JDK jce差异,有了解到,其实IBM和SUN实现SHA1PRNG也会是有差异的,不能保证IBMSecureRandom和SUN的SecureRandom,SHA1PRNG算法得到一直的结果!!! 然后就是上生产,测试环境,运行代码,查看一下这个SecureRandom的算法提供者,看看是否有差异,代码如下
SecureRandom random= SecureRandom.getInstance("SHA1PRNG");
System.out.println("默认:"+random.getProvider());
直接放到java文件中生成class文件运行或者放jsp运行,都可以,得到 生产:IBMJCE 1.2 测试1:IBMJCE 1.8 测试2:SUN version 1.8
在IBM中 SHA1PRNG由IBM JCE实现,找到生产IBM JDK 1.6下com.ibm.crypto.provider.IBMJCE类所在的jar包%JAVA_HOME%/jre/lib/ext/ibmjceprovider.jar和其依赖的%JAVA_HOME%/jre/lib/ibmpkcs.jar,引入工程
动态引入算法提供者,因为本地的JDK 是SUN 1.8的版本,刚刚也看了java.security.默认是使用SUN的SecureRandom 也就是说,在本地SUN JDK1.8的前提下,这两行代码是等同的
SecureRandom random= SecureRandom.getInstance("SHA1PRNG");
SecureRandom random= SecureRandom.getInstance("SHA1PRNG","SUN");
引入对应IBM JCE算法实现的相关jar包之后,
Security.addProvider(new com.ibm.crypto.provider.IBMJCE());
SecureRandom random= SecureRandom.getInstance("SHA1PRNG","IBMJCE");
System.out.println("默认:"+random.getProvider());
得到与生产相同的结果 IBMJCE 1.2,再本地进行加密,果然得到AES-128加密算法的秘钥与生产环境的秘钥一致,且加密后的结果,使用SUN JDK1.8(即对接方相同的JDK)无法解密,但是测试1的环境也是IBM,但IBMJCE是1.8的版本,也可以正常加解密,所以问题应该就在IBMJCE的版本上,生产环境的jdk运行配置不可以随意修改,所以理论上有两种方案 1、生产的IBMJCE升级到1.8 2、生产的算法提供者增加SUN算法提供者(动态配置就不用修改java.security) 适配的话比升级风险小一些,所以接下来测试,生产中是否已经有SUN 算法提供者,没有的话就使用代码动态增加算法提供者,指定算法提供者为SUN,测试代码如下
if (null != Security.getProvider("SUN")) {
System.out.println("SUN in use");
}else{
System.out.println("SUN not in use");
}
同样写到java文件中编译成class或者放到jsp中放上服务器运行,楼主这得到的结果为SUN not in use,所以使用代码动态增加算法提供者,指定算法提供者为SUN,增加算法提供者相关jar包到应用lib目录下 因楼主这生产是IBM jdk1.6的版本,于是找SUN jdk1.6的算法提供者rt.jar放到了应用lib目录(jdk1.8中因使用算法不同,会用到的jar包有rt.jar,jsse.jar,sunjce_provider.jar,jce.jar,视项目情况放,楼主这使用的sun.security.provider.Sun(),就在rt.jar里,放入rt.jar,就正常了所以剩下的jar就没再放入)
static{
Security.addProvider(new sun.security.provider.Sun());
}
public static byte[] generateAesKey(String key) {
byte[] keys = null;
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
keys = secretKey.getEncoded();
} catch (GeneralSecurityException e) {
logger.error(e.getMessage(), e);
}
return keys;
}
附测试jsp
<%@ page import="java.security.Security" %>
<%@ page import="javax.crypto.KeyGenerator" %>
<%@ page import="java.security.SecureRandom" %>
<%@ page import="javax.crypto.SecretKey" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public static String byteToHex(byte[] bytes) {
String strHex = "";
StringBuilder sb = new StringBuilder("");
for (int n = 0; n < bytes.length; n++) {
strHex = Integer.toHexString(bytes[n] & 0xFF);
sb.append((strHex.length() == 1) ? "0" + strHex : strHex); // 每个字节由两个字符表示,位数不够,高位补0
}
return sb.toString().trim();
}
%>
<%
// Security.addProvider(new com.ibm.crypto.provider.IBMJCE());
// Security.addProvider(new BouncyCastleProvider());
Security.addProvider(new sun.security.provider.Sun());
if (null != Security.getProvider("SUN")) {
out.println("SUN in use");
}
String key="11111111111111111";
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
//防止linux下 随机生成key
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG","SUN");
secureRandom.setSeed(key.getBytes());
keyGenerator.init(128, secureRandom);
SecretKey secretKey = keyGenerator.generateKey();
SecureRandom random= SecureRandom.getInstance("SHA1PRNG");
out.println("默认:"+random.getProvider());
out.println("系统随机数生成AES加密秘钥:"+byteToHex(secretKey.getEncoded()));
%>
</body>
</html>
附查看本地JDKProvider信息代码
import java.security.Provider;
import java.security.Security;
public class TestBouncyCastle {
public static void main(String[] args) {
Provider[] providers = Security.getProviders();
for (Provider p : providers) {
System.out.println("提供者名称:" + p.getName() + "版本号:" + p.getVersion());
System.out.println();
System.out.println(p.getInfo());
}
System.out.println();
System.out.println("支持的消息摘要名称");
for (String s : Security.getAlgorithms("messageDigest")) {
System.out.println("算法名称:" + s);
}
System.out.println("支持生成公钥和私钥的方法");
for (String s : Security.getAlgorithms("keypairGenerator")) {
System.out.println("name:" + s);
}
}
}
至此问题终于解决了 参考的博客和资料如下,可以都看看: http://www.360doc.com/content/12/0105/14/3888911_177438546.shtml https://wenku.baidu.com/view/bb6638042379168884868762caaedd3383c4b58c.html?qq-pf-to=pcqq.c2c https://bugs.openjdk.java.net/browse/JDK-4705093 https://stackoverflow.com/questions/23024107/securerandom-getinstancesha1prng-sun-always-blocking-while-new-securerand