Java中RSA加密
一. 什么是Base64?
Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一,大家可以查看RFC2045~RFC2049,上面有MIME的详细规范。
Base64要求把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。
这样说会不会太抽象了?不怕,我们来看一个例子:
转换前 aaaaaabb ccccddddeeffffff
转换后 00aaaaaa 00bbcccc 00ddddee 00ffffff
在RSA加密中的作用是将加密后的乱码内容编译成一段不乱码的文字便于传输!
二. RSA加密解密的原理和步骤
1. 不管是浏览器上,还是服务器上,或者在网络传输的过程中,我们的关键或者重要的数据都要经过加密,不能以明文的形式存放。防止被别人截取!
2. 之前用的MD5算法,只能加密,但是不能解密回原来的数据!但是RSA可以实现解密和加密,并且加密密钥和解密密钥不是同一个,简称不对称加密,安全性是有保障的!
3. 1) 原有的公钥Key文件中存放的Base64格式的公钥,那么我们读取回来进行使用,就需要先用Base64解码,获取我们原有的公钥字节
2)加密数据
3)把加密的数据提交给服务器,但是加密过的数据时一堆乱码,直接提交会有问题,所以,我们再将这些乱码用Base64进行编码
4)将来服务器收到我们的数据,需要先用Base64解码,获取到原始加密数据
5)用私钥解密
三.Java中加密的详细过程
1. 利用openssl工具获取私钥和公钥
运行openssl--àbin----àopenssl.exe程序,按照生成命令中的步骤来获取私钥和公钥,而且私钥要转换成pkcs8格式的文件。
2. 创建获取公钥和私钥的方法
public static PublicKey readPublicKey(String path) throwsNoSuchAlgorithmException, InvalidKeySpecException, IOException {
BufferedReader br = newBufferedReader(new FileReader(path));
StringBuffer sb = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null){
if(temp.startsWith("-----")) {
continue;
}
sb.append(temp);
}
byte[] base64Key =sb.toString().getBytes();
byte[] originKey =Base64.getDecoder().decode(base64Key);
X509EncodedKeySpec keySpec = newX509EncodedKeySpec(originKey);
KeyFactory keyFactory =KeyFactory.getInstance("RSA");
PublicKey publicKey =keyFactory.generatePublic(keySpec);
return publicKey;
}
//获取私钥
public static PrivateKey readPrivateKey(String path) throwsIOException, NoSuchAlgorithmException, InvalidKeySpecException {
BufferedReader br = newBufferedReader(new FileReader(path));
StringBuffer sb = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null){
if(temp.startsWith("-----")) {
continue;
}
sb.append(temp);
}
byte[] base64Key =sb.toString().getBytes();
byte[] originKey =Base64.getDecoder().decode(base64Key);
PKCS8EncodedKeySpec keySpec = newPKCS8EncodedKeySpec(originKey);
KeyFactory keyFactory =KeyFactory.getInstance("RSA");
PrivateKey privateKey =keyFactory.generatePrivate(keySpec);
return privateKey;
}
3. 加解密的方法
//1.从文件中读取公私钥
PrivateKeyprivateKey=readPrivateKey("rsa_private_key_pkcs8.pem");
PublicKeypublicKey=readPublicKey("rsa_public_key.pem");
//得到base64编译后的私钥
byte[]encode = Base64.getEncoder().encode(privateKey.getEncoded());
System.out.println(new String(encode));
//2.我们的数据加密之后是乱码的,而乱码是无法传输的
//处理成不乱码的字符串
StringcontentOrigin="ABCD123哈哈哈呵呵呵呵吼吼吼啦啦啦QQQKKKK";
//初始化公钥
Ciphercipher=Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE,publicKey);
//利用公钥加密后获取到一个字节数组 这个时候还是乱码
byte[]doFinal = cipher.doFinal(contentOrigin.getBytes());
//将乱码用base64转换为不是乱码的字节数组
byte[]encodeFinal = Base64.getEncoder().encode(doFinal);
System.out.println("加密后的数据: "+new String(encodeFinal));
//初始化私钥
Cipher cipher2=Cipher.getInstance("RSA");
cipher2.init(Cipher.DECRYPT_MODE, privateKey);
//用私钥解密base64解密后的乱码就能得到原始数据
byte[] doFinal2 =cipher2.doFinal(Base64.getDecoder().decode(encodeFinal));
System.out.println("解密回来的数据 : "+new String(doFinal2));
4. 加签验签--------加密解密只是对内容进行操作,而加签验签的作用就是验证这个客户端是否对应的这个服务器,相当于一种用户的身份验证!
//1.从文件中读取公私钥
PrivateKeyprivateKey=readPrivateKey("rsa_private_key_pkcs8.txt");
PublicKeypublicKey=readPublicKey("rsa_public_key.pem");
String content="buy=success";
//1.加签
Signature signature =Signature.getInstance("sha1WithRSA");
signature.initSign(privateKey);
signature.update(content.getBytes("GBK"));
byte[] signed = signature.sign();
//base64编码后,发送出去。。。 1.原始数据 2.加签后的数据
//base64解码
//2.验签
Signature signature2 = Signature.getInstance("sha1WithRSA");
signature2.initVerify(publicKey);
signature2.update(content.getBytes("GBK") );
boolean bverify = signature2.verify(signed);
System.out.println("验证结果: "+bverify);
web项目中的RSA加密
RSA加密
一. 首先openSSL工具生成公私钥对(方式同上)
二. 服务器存储公私钥(将公私钥存放到一个客户端访问不到的文件夹下面)
三. java中的代码应当提供:
1. 读取公钥,私钥的方法(同上)
2. 解析base64数据的方法:
应当是:new
BigInteger(encryptDatas, 16).toByteArray();
3
.
解密数据:
Cipher
要注意的是
js
和
java
中对于
RSA
数据的
padding不尽相同,故我们借助一个三方库处理成相同格式
bcprov-jdk15on-1.54.jar
可以帮我们解决上述问题
否则有
javax.crypto.BadPaddingException:Decryption error
RSA加密中padding?
padding即填充方式,由于RSA加密算法中要加密的明文是要比模数小的,padding就是通过一些填充方式来限制明文的长度。
四.
js
中应当提供:
1.
获取公钥,注意,此时
js
中并不使用原始公钥,而是使用公钥的
Module
和
empoent
首先得通过一个
servlet
来获取到这两个参数
2.
加密代码
我们使用
js
库来完成
http://www.ohdave.com/rsa/
3.
提交代码
部分数据也可以加密成功,但是有时回报如下错误:
org.bouncycastle.crypto.DataLengthException: input too large forRSA cipher.
atorg.bouncycastle.crypto.engines.RSACoreEngine.convertInput(Unknown Source)
atorg.bouncycastle.crypto.engines.RSABlindedEngine.processBlock(Unknown Source)
at org.bouncycastle.jcajce.provider.asymmetric.rsa.CipherSpi.engineDoFinal(UnknownSource)
at javax.crypto.Cipher.doFinal(Cipher.java:2087)
或者如下错误:
java.lang.IllegalArgumentException: Bad arguments
at javax.crypto.Cipher.doFinal(Cipher.java:2141)
at com.dimeng.p2p.yylh.util.SecurityHelper.RSAdecrypt(SecurityHelper.java:236)
at com.dimeng.p2p.yylh.util.SecurityHelper.getdecryptStr(SecurityHelper.java:262)
at com.dimeng.p2p.yylh.util.SecurityHelper.main(SecurityHelper.java:342)
Exception in thread "main" java.lang.IllegalArgumentException: Bad arguments
at javax.crypto.Cipher.doFinal(Cipher.java:2141)
上述2个错误的原因大体如下:
错误1:RSA有加密长度显示
错误2:准确来说是因为js加密的时候会导致byte[]类型密文比指定的长,为什么呢?因为上面提到的三个JS在加密密码时,偶尔会得出正确的密文byte[]多出一byte,里面是0。
针对错误1:
切块
针对错误2:
public static byte[] hexStringToBytes(String hexString) { if (hexString == null || hexString.equals("")) { return null; } hexString = hexString.toUpperCase(); int length = hexString.length() / 2; char[] hexChars = hexString.toCharArray(); byte[] d = new byte[length]; for (int i = 0; i < length; i++) { int pos = i * 2; d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); } return d; } private static byte charToByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); }
五. 一个完整的RSAUtil类
public classRSAUtils {
//1.读取公钥的方法上面有这里不写了
//2. 读取私钥的方法上面也有不写了
//3. 利用私钥解密的方法(服务器里面,客户端加密在Js中完成)
public static String decrypt(String base64Datas, PrivateKey privateKey) throwsNoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,BadPaddingException, IllegalBlockSizeException, IOException {
//解码base64的数据
byte[]bytes = hexStringToBytes(base64Datas);
//解密的过程。。
Ciphercipher = Cipher.getInstance("RSA",new BouncyCastleProvider());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
intblockSize = cipher.getBlockSize();
ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
int j = 0;
//分块解密
while(bytes.length - j * blockSize > 0) {
System.out.println("--------------");
System.out.println(bytes.length);
System.out.println(j * blockSize);
System.out.println(blockSize);
System.out.println("--------------");
bout.write(cipher.doFinal(bytes, j * blockSize, blockSize));
j++;
}
StringBuilder stringBuilder=new StringBuilder(newString(bout.toByteArray()));
//bout.toByteArray()有我们全部的解密数据
stringBuilder.reverse();
returnstringBuilder.toString();
}
//4. 将base64数据转换为字节数组防止Badarguments异常
public staticbyte[] hexStringToBytes(String hexString) {
if(hexString == null || hexString.equals("")) {
return null;
}
hexString =hexString.toUpperCase();
int length= hexString.length() / 2;
char[]hexChars = hexString.toCharArray();
byte[] d =new byte[length];
for (int i= 0; i < length; i++) {
int pos = i * 2;
d[i] =(byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
//5. 字符转字节的方法
private staticbyte charToByte(char c) {
return(byte) "0123456789ABCDEF".indexOf(c);
}}
六. 在访问界面的时候首先得访问一个RSAServlet,获取到js加密要用到的e,m元素
try {
RSAPublicKey publicKey = (RSAPublicKey)RSAUtils.readPublicKey(this.getServletContext().getRealPath("/key/rsa_public_key.pem"));
Stringmodule = publicKey.getModulus().toString(16);
Stringempoent = publicKey.getPublicExponent().toString(16);
request.setAttribute("m", module);
request.setAttribute("e", empoent);
request.getRequestDispatcher("index.jsp").forward(request,response);
} catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
七. js前端加密数据
//获取文本框里的值
varoriginText=document.getElementById("tx").value;
setMaxDigits(130); //131 => n的十六进制位数/2+3
var key = newRSAKeyPair("${e}","","${m}"); //e,m是从RSAServlet中获取的
//用e m公钥进行加密
var encryptValue = encryptedString(key,originText); //不支持汉字
document.getElementById("encryptDatas").value=encryptValue;
document.getElementById("form1").submit();
八. 数据会上传到一个TestServlet
//获取密文 用私钥进行解密即可
String datas =request.getParameter("encryptDatas");
try {
PrivateKeyprivateKey =RSAUtils.readPrivateKey(this.getServletContext().getRealPath("/key/rsa_private_key_pkcs8.pem"));
try {
Stringdecrypt = RSAUtils.decrypt(datas, privateKey);
System.out.println("解密后的数据:"+decrypt);
} catch(NoSuchPaddingException | InvalidKeyException | BadPaddingException |IllegalBlockSizeException e) {
e.printStackTrace(); } } catch (NoSuchAlgorithmException |InvalidKeySpecException e) {
e.printStackTrace(); }
九. 要注意的地方
要放私钥和公钥,要导入四个js包和一个jar包!
Barrett.js
BigInt.js
RSA.js
RSA_Stripped.js
bcprov-jdk15on-1.54.jar
要注意的地方:
公钥加密,私钥解密。加密的系统和解密的系统分开部署,加密的系统不应该同时具备解密的功能,这样即使黑客攻破了加密系统,他拿到的也只是一堆无法破解的密文数据。否则的话,你就要考虑你的场景是否有必要用 RSA 了。
生成密文的长度等于密钥长度。密钥长度越大,生成密文的长度也就越大,加密的速度也就越慢,而密文也就越难被破解掉。著名的"安全和效率总是一把双刃剑"定律,在这里展现的淋漓尽致。我们必须通过定义密钥的长度在"安全"和"加解密效率"之间做出一个平衡的选择。
不管明文长度是多少,RSA 生成的密文长度总是固定的。
但 是明文长度不能超过密钥长度。比如 Java 默认的 RSA 加密实现不允许明文长度超过密钥长度减去 11(单位是字节,也就是 byte)。也就是说,如果我们定义的密钥(我们可以通过 java.security.KeyPairGenerator.initialize(int keysize) 来定义密钥长度)长度为 1024(单位是位,也就是 bit),生成的密钥长度就是 1024位 / 8位/字节 = 128字节,那么我们需要加密的明文长度不能超过 128字节 -
11 字节 = 117字节。也就是说,我们最大能将 117 字节长度的明文进行加密,否则会出问题(抛诸如 javax.crypto.IllegalBlockSizeException:Data must not be longer than 53 bytes 的异常)。
而 BC 提供的加密算法能够支持到的 RSA 明文长度最长为密钥长度。
警惕toString 陷阱:Java 中数组的 toString() 方法返回的并非数组内容,它返回的实际上是数组存储元素的类型以及数组在内存的位置的一个标识。
大部分人跌入这个误区而不自知,包括一些写了多年Java 的老鸟。比如这篇博客《How To Convert Byte[] Array To String In Java》中的代码
[java] viewplain copy
print?
输出:
Text : This is an example
Text [Byte Format] : [B@187aeca
Text [Byte Format] : [B@187aeca
Text Decryted : This is an example
以及这篇博客《RSA Encryption Example》中的代码
[java] viewplain copy
print?
java.lang.String 保存明文,byte 数组保存二进制密文,在 java.lang.String 和 byte[] 之间不应该具备互相转换。如果你确实必须得使用 java.lang.String 来持有这些二进制数据的话,最安全的方式是使用 Base64(推荐 Apache 的 commons-codec 库的 org.apache.commons.codec.binary.Base64):
[java] viewplain copy
print?
一个优秀的加密必须每次生成的密文都不一致,即使每次你的明文一样、使用同一个公钥。因为这样才能把明文信息更安全地隐藏起来。
Java 默认的 RSA 实现是 "RSA/None/PKCS1Padding"(比如 Cipher cipher =Cipher.getInstance("RSA");句,这个 Cipher 生成的密文总是不一致的),Bouncy Castle 的默认 RSA 实现是 "RSA/None/NoPadding"。
为什么 Java 默认的 RSA 实现每次生成的密文都不一致呢,即使每次使用同一个明文、同一个公钥?这是因为 RSA 的 PKCS #1 padding 方案在加密前对明文信息进行了随机数填充。
你可以使用以下办法让同一个明文、同一个公钥每次生成同一个密文,但是你必须意识到你这么做付出的代价是什么。比如,你可能使用 RSA 来加密传输,但是由于你的同一明文每次生成的同一密文,攻击者能够据此识别到同一个信息都是何时被发送。
[java] viewplain copy
print?
Java 默认的 RSA 实现"RSA/None/PKCS1Padding" 要求最小密钥长度为 512 位(否则会报 java.security.InvalidParameterException:RSA keys must be at least 512 bits long 异常),也就是说生成的密钥、密文长度最小为 64 个字节。如果你还嫌大,可以通过调整算法提供者来减小密文长度:
[java] viewplain copy
print?
如此这般得到的密文长度为 128 位(16 个字节)。但是这么干之前请先回顾一下本文第 2 点所述。
javax.crypto.Cipher 是有状态的,不要把 Cipher当做一个静态变量,除非你的程序是单线程的,也就是说你能够保证同一时刻只有一个线程在调用 Cipher。否则你可能会像笔者似的遇到 java.lang.ArrayIndexOutOfBoundsException:too much data for RSA block 异常。遇见这个异常,你需要先确定你给 Cipher加密的明文(或者需要解密的密文)是否过长;排除掉明文(或者密文)过长的情况,你需要考虑是不是你的 Cipher 线程不安全了。