因爲公司的網站頁面的表單提交是明文的post,雖然說是https的頁面,但仍是有點隱患(https會不會被黑?反正明文逼格是差了點你得認可啊),因此上頭吩咐我弄個RSA加密,客戶端JS加密,而後服務器JAVA解密。javascript
本文主要面向想在javaweb/java應用裏面使用RSA的人。html
其實一開始叫我用RSA加密我是拒絕的,由於不可能你叫我用我就用,我得查查他是什麼東西對不對。前端
RSA是目前最有影響力的公鑰加密算法,屬於非對稱加密的,也就是用一個你們都知道的公鑰來加密出來的密文,只有擁有私鑰的人才能解開,目前據說1024比較安全,2048位那是至關安全,往上就更難破解了。java
關於RSA的基本原理,百度百科裏面提到「RSA算法基於一個十分簡單的數論事實:將兩個大素數相乘十分容易,可是想要對其乘積進行因式分解卻極其困難,所以能夠將乘積公開做爲加密密鑰。」而後阮一峯先生的 《RSA算法原理(一)》和《RSA算法原理(二)》裏面就解釋的比較清楚,看完你大概就懂了(不愧是大牛吖)。阮先生提到了:git
加密公式:web
me ≡ c (mod n)ajax
解密公式:算法
cd ≡ m (mod n)api
n是公共的模數,也就是pq的乘積,e是公鑰的指數,d是私鑰的指數,下面會說到利用他們能夠還原公鑰和密鑰。瀏覽器
若是你知道是怎麼回事的話也就清楚了RSA是怎麼回事, 不清楚的話請去看文章,這裏主要是說如何去使用RSA而不是本身實現RSA。知道RSA的大概原理將會對咱們寫代碼有幫助,否則你出了問題本身都搞不定,咋辦?
提及思路比較簡單。其實就是瀏覽器向服務器拿到公鑰,在用戶填完信息後,用公鑰幫用戶加密,而後提交後,服務器再用java解密就能夠了…….嗎?原本大致思路是這樣沒錯,但是我找到的方法並無辦法直接傳輸可用的公鑰,因此咱們要用到上述的公式,先把e和n取出來,利用js把e和n還原成公鑰,而後再用他給信息加密後提交,最後就是用服務器上的java解密。
說了那麼多,福利呢乾貨呢別人寫好的代碼呢?沒錯我就知道你想要這個。
一開始我也是大概搞清楚RSA以後開始各類找代碼來參考一下,因而在我不懈努力之下,讓我找到了這個《用javascript與java進行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的相關類和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);
至此出錯問題就能解決了