一、问题描述
系统服务端与客户端之间的通信需要进行签名和加密传输。
签名算法:SHA1WithRSA,采用SUN JDK的keytool工具生成公私钥对证书(具体生成方法可自行百度)。密钥长度2048。
加解密算法:AES,分组模式ECB,填充方式PKCS5Padding。
(之所以采用对称加密,是因为速度快,RSA非对称加密速度很慢。)
服务端基于SUN JDK 1.8_161开发,服务端AES加解密的关键代码段如下:
private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
private static final String AES_SEED = "AESSeedForTest2019";
/**
* 进行AES加解密
* @param dataBytes 加解密前的数据
* @param mode 加密/解密模式:加密-Cipher.ENCRYPT_MODE,解密-Cipher.DECRYPT_MODE
* @return 加解密后的数据
*/
private byte[] doAES(byte[] dataBytes, int mode){
byte[] dataAfter = null;
try
{
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(128, new SecureRandom(AES_SEED.getBytes()));
SecretKey secretKey = keyGenerator.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(mode, secretKeySpec);
dataAfter = cipher.doFinal(dataBytes);
} catch (Exception e){
e.printStackTrace();
}
return dataAfter;
}
为方便其他系统快速接入服务器,开发了客户端,客户端与服务端的AES加解密代码相同。
当客户端系统使用SUN JDK 1.8.0_161及以上版本时,加解密正常。
问题1: 当客户端系统使用SUN JDK 1.8.0_161以下版本或者IBM JDK时,签名时会报“Illegal Key Size”错误。
问题2: 客户端系统使用AIX系统,IBM JDK 1.5版本。加解密时始终异常,报“PaddingException: Given final block not properly padded”。
由于此时已有一些系统使用了该客户端接入服务,且运行正常,为保证这些系统的正常,解决这些问题最好不要修改服务端,或者至少保证修改是兼容原来的客户端的。
二、问题原因
对于问题1,以前项目遇到过,是因为JDK出口限制,需要到JDK官网下载不受限制的策略jar包,替换%JAVA_HOME%\jre\lib\security下同名字的两个jar文件(无论如何,替换前记得备份原文件!)。
例如,对于SUN JDK 1.8,下载地址:
https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
对于IBM JDK,下载地址:
https://www-01.ibm.com/marketing/iwm/iwm/web/pickUrxNew.do?source=jcesdk
对于问题2,只知道肯定是因为客户端和服务器加解密不一致导致的,但是究竟是哪里导致的不一致不清楚。网上大多解释是因为代码中没有指定算法的Provider(提供者),不同JDK都按照JCE(Java Cryptography Extension)规范进行实现,为什么会不一样呢?
三、问题解决
按照网上的解决方法进行了如下尝试:
1. 引入算法第三方的提供者bouncy castle(jar包下载地址:http://www.bouncycastle.org/latest_releases.html)
首先需要将bouncy castle加入提供者列表中,例如可以这样:
static{
Security.addProvider(new BouncyCastleProvider());
}
然后代码中指定算法Provider为BC:
/**
* 进行AES加解密
* @param dataBytes 加解密前的数据
* @param mode 加密/解密模式:加密-Cipher.ENCRYPT_MODE,解密-Cipher.DECRYPT_MODE
* @return 加解密后的数据
*/
private byte[] doAES(byte[] dataBytes, int mode){
byte[] dataAfter = null;
try
{
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM,"BC");
keyGenerator.init(128, new SecureRandom(AES_SEED.getBytes()));
SecretKey secretKey = keyGenerator.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM,"BC");
cipher.init(mode, secretKeySpec);
dataAfter = cipher.doFinal(dataBytes);
} catch (Exception e){
e.printStackTrace();
}
return dataAfter;
}
经过测试,如果客户端是SUN JDK,即使服务端程序不变,仅客户端改为BC提供者,通信加解密正常。但是如果是放到AIX+IBM JDK 1.5下,报异常:
error constructing MAC: java.lang.SecurityException: JCE cannot authenticate the provider BC
尝试按照网上说法,修改%JAVA_HOME%/jre/lib/security/java.security,将Bouncy Castle加入到授信的提供者中:
security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider
事实证明,这种方法在AIX + IBM JDK 1.5下并不生效(至于其它版本是否可行不清楚)!
2. 按照https://blog.csdn.net/afer198215/article/details/11672035所述,在IBM JDK下加载SUN提供者,由于客户端的IBM JDK是1.5版本,需要1.5版本的SUN JDK的jar包,无奈无法在SUN JDK1.5下找到对应算法的提供者。
3. 最后经过不知道多少次尝试,定位是因为SUN JDK和IBM JDK在SecureRandom算法实现上的差异导致。过程如下:
经测试, 下面的写法在SUN JDK中默认采用的是SHA1PRNG随机数生成算法:
new SecureRandom(AES_SEED.getBytes());
在IBM JDK中同样实现了这个算法,但不清楚这样写IBM JDK是否同样默认使用SHA1PRNG算法,因此改为指定SHA1PRNG生成随机数:
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(AES_SEED.getBytes());
keyGenerator.init(128, random);
测试发现报同样的错,难道IBM JDK和SUN JDK的SHA1PRNG使用同样的种子算法输出结果不同?带着疑问查找,找到一篇相关文章写得很好:
http://lampwww.epfl.ch/java/jre-ibm-1.5/docs/common/securityguide.lnx.htm
文章中提到:
The Java security configuration file does not refer to the Sun provider. The IBM JCE provider has replaced the Sun provider. The JCE supplies all the signature handling message digest algorithms that were previously supplied by the Sun provider. It also supplies the IBM secure random number generator, IBMSecureRandom, which is a real Random Number Generator. SHA1PRNG is a Pseudo Random Number Generator and is supplied for code compatibility. SHA1PRNG is not guaranteed to produce the same output as the SUN SHA1PRNG.
IBM实现的SHA1PRNG不保证和SUN实现的SHA1PRNG有相同的输出结果!
最后想想,是否可以将IBM的算法实现运行在SUN JDK呢?
为此,进行最后一次尝试,根据上面找到的文章知道,SHA1PRNG由IBM JCE实现,找到IBM JDK 1.5下com.ibm.crypto.provider.IBMJCE类所在的jar包%JAVA_HOME%/jre/lib/ext/ibmjceprovider.jar和其依赖的%JAVA_HOME%/jre/lib/ibmpkcs.jar。将其放到服务端工程目录下,在程序中动态添加IBMJCE算法提供者:
static{
Security.addProvider(new IBMJCE());
}
根据客户端的实际运行JDK供应商使用相应的SHA1PRNG算法提供者,当客户端是SUN JDK时,
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
当客户端是IBM JDK时,
SecureRandom random = SecureRandom.getInstance("SHA1PRNG","IBMJCE");
至此,问题得以解决。
四、 遗留疑问
1. SHA1PRNG随机数生成算法没有第三方供应商吗?
2. bouncy castle在AIX+IBM JDK1.5下为什么就不能用呢?
参考:
IBM-JDK与Sun-JDK加密算法提供者差异: https://blog.csdn.net/afer198215/article/details/11672035
https://blog.csdn.net/xiongzk/article/details/8102946
JCE cannot authenticate the provider BC 错误: https://www.cnblogs.com/zhongkaiuu/p/BC.html
IBM JDK安全指引规范:http://lampwww.epfl.ch/java/jre-ibm-1.5/docs/common/securityguide.lnx.htm