寫在前面
本文是在解決加密和解密用的不是同一套密鑰對時找到的一篇, 最后問題不在byte數組, 是自己工具類中生成密鑰對的問題, 但是本文RSA加密中公鑰指數和公鑰系數的獲取(byte[]部分)講解比較細致, 雖然最后也沒用這種方式. 以下是本人采用的方式. 位數確實不對, 但是不影響前台根據系數和指數生成公鑰, 也不影響后台解密, 僅僅做一個記錄.
// // 獲取公鑰系數和公鑰指數 // // 獲取公鑰對象--注意:前端那邊需要用到公鑰系數和指數 // RSAPublicKey publicKey = RSAUtils.getDefaultPublicKey(); // // 公鑰-系數(n) // request.setAttribute("pkModulus", new String(Hex.encode(publicKey.getModulus().toByteArray()))); // // 公鑰-指數(e1) // request.setAttribute("pkExponent", new String(Hex.encode(publicKey.getPublicExponent().toByteArray()))); // 獲取公鑰系數和公鑰指數 //公鑰-系數(n) RSAPublicKey publicKey = RSAUtils.aPublic; String pkModulus2 = publicKey.getModulus().toString(16); request.setAttribute("pkModulus", pkModulus2); //公鑰-指數(e1) String pkExponent2 = publicKey.getPublicExponent().toString(16); request.setAttribute("pkExponent", pkExponent2);
BigInteger轉為16進制的String方式對比
方式一
String pkModulus2 = publicKey.getModulus().toString(16);生成的公鑰系數(256位)
e0ffbc04ee1c099b3e898359a12a3d1a307415ec3daaff86e3d1b61a7d434e5073de79bc7de12324d4643fb93923f007897c35b7bb98c2864b8e4d319a5028935f882fad6ba1df8181478a331cf7d59335a603262bf7ad5aa648869ebd348640ad95f389eb603b6e301ea3e7aff24dc58209c2eef449a2bbe8d6d2159cdf1383
方式二
String pkModulus = new String(Hex.encode(publicKey.getModulus().toByteArray()));生成的公鑰系數258位, 區別就是前面多的00
00e0ffbc04ee1c099b3e898359a12a3d1a307415ec3daaff86e3d1b61a7d434e5073de79bc7de12324d4643fb93923f007897c35b7bb98c2864b8e4d319a5028935f882fad6ba1df8181478a331cf7d59335a603262bf7ad5aa648869ebd348640ad95f389eb603b6e301ea3e7aff24dc58209c2eef449a2bbe8d6d2159cdf1383
BigInteger的toString(int radix)方法
/** * Returns the String representation of this BigInteger in the * given radix. If the radix is outside the range from {@link * Character#MIN_RADIX} to {@link Character#MAX_RADIX} inclusive, * it will default to 10 (as is the case for * {@code Integer.toString}). The digit-to-character mapping * provided by {@code Character.forDigit} is used, and a minus * sign is prepended if appropriate. (This representation is * compatible with the {@link #BigInteger(String, int) (String, * int)} constructor.) * * @param radix radix of the String representation. * @return String representation of this BigInteger in the given radix. * @see Integer#toString * @see Character#forDigit * @see #BigInteger(java.lang.String, int) */ public String toString(int radix) { if (signum == 0) return "0"; if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; // If it's small enough, use smallToString. if (mag.length <= SCHOENHAGE_BASE_CONVERSION_THRESHOLD) return smallToString(radix); // Otherwise use recursive toString, which requires positive arguments. // The results will be concatenated into this StringBuilder StringBuilder sb = new StringBuilder(); if (signum < 0) { toString(this.negate(), sb, radix, 0); sb.insert(0, '-'); } else toString(this, sb, radix, 0); return sb.toString(); }
jdk1.8中描述
-
public String toString(int radix)返回給定基數中BigInteger的String表示形式。 如果基數在Character.MIN_RADIX到Character.MAX_RADIX之間,則默認為10(如Integer.toString)。 使用由Character.forDigit提供的數字到字符的映射,如果合適,則添加減號。 (此表示與(String, int)構造函數兼容。)- 參數
-
radix- 字符串表示的基數。 - 結果
- 此BigInteger在給定基數中的字符串表示形式。
- 另請參見:
-
Integer.toString(int, int),Character.forDigit(int, int),BigInteger(java.lang.String, int)
聲明
以下內容轉自:JavaWeb對RSA的使用
由於公司的網站頁面的表單提交是明文的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其實和加密包會有關系。
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是這樣的
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
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,不信等報錯了你自己試試。解決方法如下:
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);
至此出錯問題就能解決了
