[Just a door] java web对RSA的使用

今晚新开[Just a door]系列,出发点其实是每次项目需要解决一个问题或者找一方面的资料的时候,查百度都是一堆我们不生产答案,我们只是答案的搬运工心态的东西,一个问题的答案被各种胡乱复制粘贴,根本治标不治本,甚至各种错漏。对一个程序员来说能忍?

于是这个系列不去抄东西,也不是很表面的“只要把这段代码复制上去就好了”,而是根据文章布局引用些有营养的链接,让博文作为一个问题比较全面的总结,尽量做到举一反三。每篇文章只是你学习该模块的一个门一个入口(虽然以我的尿性很容易这个系列就只有这一篇了orz

反正像我这种技术渣也没有研究太多底层的东西,我们就冲着完成任务并且学到东西,就从各方面收集好资料,整理好总结好,然后服务好各位观众老爷就好。于是开始:


力求写的全面点,自己已经知道的部分请忽略_(:з」∠)_

QQ截图20150402114440

由于公司的网站页面的表单提交是明文的post,虽说是https的页面,但还是有点隐患(https会不会被黑?反正明文逼格是差了点你得承认啊),所以上头吩咐我弄个RSA加密,客户端JS加密,然后服务器JAVA解密

本文主要面向想在javaweb/java应用 里面使用RSA的人。

 

一、RSA是个ShenMeGui:

其实一开始叫我用RSA加密我是拒绝的(屁),因为不可能你叫我用我就用,我得查查他是什么东西对不对。

RSA是目前最有影响力的公钥加密算法,属于非对称加密的,也就是用一个大家都知道的公钥来加密出来的密文,只有拥有私钥的人才能解开,目前听说1024比较安全,2048位那是相当安全,往上就更难破解了。

关于RSA的基本原理,百度百科里面提到“RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。”然后阮一峰先生的 《RSA算法原理(一)》《RSA算法原理(二)》里面就解释的比较清楚,看完你大概就懂了(不愧是大牛吖)。阮先生提到了:

加密公式

me ≡ c (mod n)

解密公式

cd ≡ m (mod n)

n是公共的模数,也就是pq乘积e是公钥的指数d是私钥的指数,下面会说到利用他们可以还原公钥和密钥。

如果你知道是怎么回事的话也就清楚了RSA是怎么回事, 不清楚的话请去看文章,这里主要是说如何去使用RSA而不是自己实现RSA。知道RSA的大概原理将会对我们写代码有帮助,不然你出了问题自己都搞不定,咋办?

二、使用RSA实现加密解密的思路:

说起思路比较简单。其实就是浏览器向服务器拿到公钥,在用户填完信息后,用公钥帮用户加密,然后提交后,服务器再用java解密就可以了…….吗?本来大体思路是这样没错,可是我找到的方法并没有办法直接传输可用的公钥,所以我们要用到上述的公式,先把e和n取出来,利用js把e和n还原成公钥,然后再用他给信息加密后提交,最后就是用服务器上的java解密。

三、伸手党时间:

说了那么多,福利呢干货呢别人写好的代码呢?没错我就知道你想要这个。

一开始我也是大概搞清楚RSA之后开始各种找代码来参考一下,于是在我不懈努力之下,让我找到了这个《用javascript与java进行RSA加密与解密》,总体还是可用的,而且总体思路和我一致,但是用起来问题还是多多的,你们可以先点开链接看看代码。以下把这个链接称为“伸手党”资料。(伸手党是说你跟我,不是说那文章作者)

四、javascript里面的RSA:

首先你需要三个js(这个在“伸手党时间”里面有)

BigInt.js  – 用于生成一个大整型;(这是RSA算法的需要)
RSA.js    – RSA的主要算法;
Barrett.js – RSA算法所需要用到的一个支持文件;

像上面提到的,你拥有公钥的n和e就能还原公钥,然后进行加密。关键代码如下:

//setMaxDigits()貌似是生成密文的最大位数,如果不设置或者乱设置的话很可能导致死循环然后浏览器崩溃。
//这个语句必须最先执行,1024位的密钥要传入130,2048的话要传入260
setMaxDigits(130);
//RSAKeyPair是密钥对的对象,用e和n可以生成公钥,第二个参数其实就是d,因为我们只需要公钥当然是传空的。
key = new  RSAKeyPair(e,"",n);
//下面是加密方法,比较简单,就是传入公钥和原文,加密成密文。
var result = encryptedString(key, document.getElementById("pwd").value);

e和n哪里来?你可以先用着链接里面的来test一下,我认为正常来说应该是java读取了送到给js的,下面会讲到。

五、 java里面的RSA:

那java里面肯定也有RSA的相关类和API能用,我们需要些什么呢?我们需要的是JDK里面的java.security包和一个开源的加解密解决方案BouncyCastle的API。java.security自己import就可以,→bouncyCastle在这里←    要JDK15以上的,下最新的就可以,打不开或者找不到的就百度一下咯~

下面我们说说java里面的代码问题,“伸手党”链接中的方法我们都可以自己修改逻辑和参数列表后就直接用了。

先来说生成密钥,利用generateKeyPair()和saveKey(),可以很简单的生成一对密钥,你可以直接存到相应路径,也可以像我一样改一下saveKey()把公钥私钥分开储存,甚至你可以直接保存在session,这样你的网站就永远在用别人猜不到的密钥了(我估计这样服务器工作量略大,还不如定时线程来更新密钥),下面是我修改过的代码

public static KeyPair generateKeyPair() throws Exception {  
    try {  
       KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider());  
       final int KEY_SIZE = 1024;// 块加密的大小,你可以改成2048,但是会很慢........
       keyPairGen.initialize(KEY_SIZE, new SecureRandom());  
       KeyPair keyPair = keyPairGen.generateKeyPair();  
       return keyPair;  
    } catch (Exception e) {  
    throw new Exception(e.getMessage());  
    }  
}

再说取钥的问题,你可以使用getKeyPair()获取公钥和密钥,大概长这样(我同事帮忙修改了下,返回的object然后自自行强制转换即可)这里注意readObject其实和加密包会有关系。

public static Object getKey(String path)throws Exception{  
     FileInputStream fis = new FileInputStream(path);  
     ObjectInputStream oos = new ObjectInputStream(fis);  
     Object kp=  oos.readObject();  
     oos.close();  
     fis.close();  
     return kp;  
}   
//使用他来获取密钥,如果你这个路径对应的是公钥或者密钥,也可以直接取出来再强制转换成RSAPublicKey或者RSAPrivateKey
	KeyPair kp=(KeyPair)getKey("D:/key.key");

我们读取到公钥后,需要用到RSAPublicKey的getModulus()和getPublicExponent()方法取得公钥的e(Exponent)和n(Modulus)给到前端页面,前端可以用getparameter等方法接收,或者在页面初始化时用ajax请求。

String Modulus=RSAPublicKey.getModulus().toString(16);
String Exponent=RSAPublicKey.getPublicExponent().toString(16);

这里需要toString(16)把他转为16进制,供前端使用。

最后是最重要的解密部分(加密和解密的写法极其相似,有需要的同学可以先看看解密),网上最简单而典型的写法是这样的(raw是密文)

public static byte[] RSAdecrypt(PrivateKey pk, byte[] raw) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider()); 
    cipher.init(Cipher.DECRYPT_MODE, pk);  
    return cipher.doFinal(raw); 
}

然后我在“伸手党”资料找到demo是这样的

public static byte[] RSAdecrypt(PrivateKey pk, byte[] raw) throws Exception {
		Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
		cipher.init(Cipher.DECRYPT_MODE, pk);		
		ByteArrayOutputStream bout = null;
		try {
			bout = new ByteArrayOutputStream(64);
			int j = 0;
			int blockSize = cipher.getBlockSize();
			while (raw.length - j * blockSize > 0) {
				bout.write(cipher.doFinal(raw, j * blockSize, blockSize));
				j++;
			}
			return bout.toByteArray();
		} //后面是catch和finally,对bout 的安全处理,就不贴了
	}

这样看貌似第二个安全一点,不过我不是很懂为什么要写的这么复杂,测了下运行时间貌似差不多。求各位指导。

这里用到加密算法最核心的类Cipher类,我觉得有必要大概了解一下它,它的加密和解密过程用到的都是doFinal()方法,至于是加密还是解密就取决于init时候的参数Cliper的MODE。然后如果没有我说的BouncyCastle API你就要用 Cipher.getInstance(keyFactory.getAlgorithm())来实例化Clipher了,会麻烦一点。

到这里加密解密就已经完成了~~~然后接下来可能会遇到一些问题,请看↓

六、遇到问题了吧,说好的售后服务呢?

(1)首先再提一下setMaxDigits(),要注意里面的参数,1024位对应130,2048位对应260。

(2)然后是java里面关于解密时参数发送异常的问题:如果完全按照上述方法,或者是“伸手党”资料里面的做法,在大量使用的情况下就会出现以下报错。

如果你是用精简版的RSAdecrypt

org.bouncycastle.crypto.DataLengthException: input too large for RSA cipher.
at org.bouncycastle.crypto.engines.RSACoreEngine.convertInput(Unknown Source)
at org.bouncycastle.crypto.engines.RSABlindedEngine.processBlock(Unknown Source)
at org.bouncycastle.jcajce.provider.asymmetric.rsa.CipherSpi.engineDoFinal(Unknown Source)
at javax.crypto.Cipher.doFinal(Cipher.java:2087)

如果你是用复杂版的RSAdecrypt

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)

原因是 “伸手党”资料里面的用法是这样的,问题就出在toByteArray()上面

//result是字符串类型的密文
byte[] en_result = new BigInteger(result, 16).toByteArray();  
byte[] de_result = RSAUtil.decrypt(RSAUtil.getKeyPair().getPrivate(),en_result);  

准确来说是因为js加密的时候会导致byte[]类型密文比指定的长,为什么呢?因为上面提到的三个JS在加密密码时,偶尔会得出正确的密文byte[]多出一byte,里面是0,不信等报错了你自己试试。网上提供的思路如下:

/** * 16进制 To byte[] * @param hexString * @return byte[] */
   	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;
   	}

   	/** * Convert char to byte * @param c char * @return byte */
   	private static byte charToByte(char c) {
   		return (byte) "0123456789ABCDEF".indexOf(c);
   	}
//这样~就对了
byte[] en_result = hexStringToBytes(password_en);

不过比较烦的是我不太懂位运算= =看懂d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));的给我解释一下谢谢~~~

七、扩展一下,加量不加价!

有同学是用.NET的,有没有例子呢?有!→《用RSA加密实现Web登录密码加密传输》(.NET版)如果使用过程中遇到问题可以参考本文

然后《Java 进行 RSA 加解密时不得不考虑到的那些事儿》这篇文章里面提到一些需要注意的东西,比如说加密的质量啊之类的,值得一看。

另外我发现梁栋老师写的《Java加密与解密的艺术(第2版)》有在线版可以点开,还有我找到了他的博客,哇好多java安全方面东西可以学习  Snowolf的博客中的Java/Security

Article written by

16 Responses

  1. lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
    lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll at | | Reply

    bug虫大让我来踩一脚你的窝!!

  2. 江湖小蝦米
    江湖小蝦米 at | | Reply

    https会对传输数据进行加密处理,所以传输的就不会是明文了_(:3」∠)_

  3. 灌水的~
    灌水的~ at | | Reply

    好像很吊哦~~~

  4. 大D
    大D at | | Reply

    你这都是上半年的博文了,都荒的全是草了,还不写点东西除除草

  5. 豆腐
    豆腐 at | | Reply

    说好的系列呢!!!??(敲碗

  6. 奇格肖
    奇格肖 at | | Reply

    谢谢博主分享。另外那个:
    (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])),应该是十六进制(4bit)转字节(8bit),2个十六进制合成一个字节,0和1,,2和3,以此类推吧

  7. 高飞飞
    高飞飞 at | | Reply

    前辈您好,我发现虽然已经改了,但是如果加密的长度太大也会报input too large for RSA cipher,

  8. 高飞飞
    高飞飞 at | | Reply

    前辈您好,我已经按照您这种改正了,但是我发现,如果加密长度太长也会报;input too large for RSA cipher

  9. dipoo
    dipoo at | | Reply

    厉害了,大神!通俗易懂,还有售后,让人少走很多弯路,666

Please comment with your real name using good manners.

Leave a Reply