目錄:
一、什麼是RSA加密算法:
二、RSA加密過程:
三、RAS解密過程:
四、生成密鑰對:
五、實踐:
六、Java進行 RSA 加解密時不得不考慮到的那些事兒:
一、什麼是RSA加密算法:
RSA加密算法是一種非對稱加密算法,所謂非對稱,就是指該算法加密和解密使用不同的密鑰,即使用加密密鑰進行加密、解密密鑰進行解密。在RAS算法中,加密密鑰(即公開密鑰)PK是公開資訊,而解密密鑰(即秘密密鑰)SK是需要保密的。加密算法E和解密算法D也都是公開的。雖然解密密鑰SK是由公開密鑰PK決定的,由于無法計算出大數n的歐拉函數phi(N),是以不能根據PK計算出SK。
也就是說,對極大整數做因數分解的難度決定了RSA算法的可靠性。理論上,隻要其鑰匙的長度n足夠長,用RSA加密的資訊實際上是不能被解破的。
RSA算法通常是先生成一對RSA密鑰,其中之一是保密密鑰,由使用者儲存;另一個為公開密鑰,可對外公開。為提高保密強度,RSA密鑰至少為500位長,一般推薦使用1024位。這就使加密的計算量很大。為減少計算量,在傳送資訊時,常采用傳統加密方法與公開密鑰加密方法相結合的方式,即資訊采用改進的DES或IDEA密鑰加密,然後使用RSA密鑰加密對話密鑰和資訊摘要。對方收到資訊後,用不同的密鑰解密并可核對資訊摘要。
RSA密鑰長度随着保密級别提高,增加很快。下表列出了對同一安全級别所對應的密鑰長度。
保密級别 | 對稱密鑰長度(bit) | RSA密鑰長度(bit) | ECC密鑰長度(bit) | 保密年限 |
---|---|---|---|---|
80 | 80 | 1024 | 160 | 2010 |
112 | 112 | 2048 | 224 | 2030 |
128 | 128 | 3072 | 256 | 2040 |
192 | 192 | 7680 | 384 | 2080 |
256 | 256 | 15360 | 512 | 2120 |
二、RSA加密過程:
RSA的加密過程可以使用一個通式來表達:

也就是說RSA加密是對明文的E次方後除以N後求餘數的過程。從通式可知,隻要知道E和N任何人都可以進行RSA加密了,是以說E、N是RSA加密的密鑰,也就是說E和N的組合就是公鑰,我們用(E,N)來表示公鑰:

不過E和N不并不是随便什麼數都可以的,它們都是經過嚴格的數學計算得出的,關于E和N擁有什麼樣的要求及其特性後面會講到。E是加密(Encryption)的首字母,N是數字(Number)的首字母。
三、RAS解密過程:
RSA的解密同樣可以使用一個通式來表達:

也就是說對密文進行D次方後除以N的餘數就是明文,這就是RSA解密過程。知道D和N就能進行解密密文了,是以D和N的組合就是私鑰:

從上述可以看出RSA的加密方式和解密方式是相同的,加密是求“E次方的mod N”;解密是求“D次方的mod N”。此處D是解密(Decryption)的首字母;N是數字(Number)的首字母。
小結下:
公鑰 | (E,N) |
---|---|
私鑰 | (D,N) |
密鑰對 | (E,D,N) |
加密 | 密文=明文EmodN密文=明文EmodN |
解密 | 明文=密文DmodN明文=密文DmodN |
四、生成密鑰對:
既然公鑰是(E,N),私鑰是(D,N),是以密鑰對即為(E,D,N),但密鑰對是怎樣生成的?步驟如下:
- 求N
- 求L(L為中間過程的中間數)
- 求E
- 求D
4.1 求N:
準備兩個互質數p,q。這兩個數不能太小,太小則會容易破解,将p乘以q就是N。如果互質數p和q足夠大,那麼根據目前的計算機技術和其他工具,至今也沒能從N分解出p和q。換句話說,隻要密鑰長度N足夠大(一般1024足矣),基本上不可能從公鑰資訊推出私鑰資訊。
N = p * q
4.2 求L:
L 是 p-1 和 q-1的最小公倍數,可用如下表達式表示
L = lcm(p-1,q-1)
4.3 求E:
E必須滿足兩個條件:E是一個比1大比L小的數,E和L的最大公約數為1;
用gcd(X,Y)來表示X,Y的最大公約數則E條件如下:
1 < E < L
gcd(E,L)=1
之是以需要E和L的最大公約數為1,是為了保證一定存在解密時需要使用的數D。現在我們已經求出了E和N也就是說我們已經生成了密鑰對中的公鑰了。
4.4 求D:
數D是由數E計算出來的,數D必須保證足夠大。D、E和L之間必須滿足以下關系:
1 < D < L
E*D mod L = 1
隻要D滿足上述2個條件,則通過E和N進行加密的密文就可以用D和N進行解密。簡單地說條件2是為了保證密文解密後的資料就是明文。
現在私鑰自然也已經生成了,密鑰對也就自然生成了。
小結:
求N | N= p * q ;p,q為質數 |
---|---|
求L | L=lcm(p-1,q-1) ;L為p-1、q-1的最小公倍數 |
求E | 1 < E < L,gcd(E,L)=1;E,L最大公約數為1(E和L互質) |
求D | 1 < D < L,E*D mod L = 1 |
五、實踐:
為了計算友善,p q 的值取小一旦,假設:p = 17,q = 19,
則:
(1)求N:N = p * q = 323;
(2)求L:L = lcm(p-1, q-1)= lcm(16,18) = 144,144為16和18對最小公倍數;
(3)求E:1 < E < L ,gcd(E,L)=1,即1 < E < 144,gcd(E,144) = 1,E和144互為質數,E = 5顯然滿足上述2個條件,故E = 5,此時公鑰= (E,N)=(5,323);
(4)求D:求D也必須滿足2個條件:1 < D < L,E*D mod L = 1,即1 < D < 144,5 * D mod 144 = 1,顯然當D= 29 時滿足上述兩個條件。1 < 29 < 144,5*29 mod 144 = 145 mod 144 = 1,此時私鑰=(D,N)=(29,323);
(5)加密:準備的明文必須是小于N的數,因為加密或者解密都要 mod N,其結果必須小于N。
假設明文 = 123,則 密文=(123的5次方)mod 323=225
(6)解密:明文=(225的29次方)mod 323 =123,是以解密後的明文為123。
六、Java進行 RSA 加解密時不得不考慮到的那些事兒:
1、質數的選擇:
首先要使用機率算法來驗證随機産生的大的整數是否是質數,這樣的算法比較快而且可以消除掉大多數非質數。假如有一個數通過了這個測試的話,那麼要使用一個精确的測試來保證它的确是一個質數。除此之外這樣找到的p和q還要滿足一定的要求,首先它們不能太靠近,此外p-1或q-1的因子不能太小,否則的話N也可以被很快地分解。
尋找質數的算法不能給攻擊者任何資訊,比如這些質數是怎樣找到的?尤其産生随機數的軟體必須非常好。要求是随機和不可預測。這兩個要求并不相同。一個随機過程可能可以産生一個不相關的數的系列,但假如有人能夠預測出(或部分地預測出)這個系列的話,那麼它就已經不可靠了。比如有一些非常好的随機數算法,但它們都已經被發表,是以它們不能被使用,因為假如一個攻擊者可以猜出p和q一半的位的話,那麼他們就已經可以輕而易舉地推算出另一半。
2、RSA加密算法的缺點:
(1)産生密鑰很麻煩,受到質數産生技術的限制,因而難以做到一次一密;
(2)運算速度慢:由于進行的都是大數計算,使得RSA最快的情況也比DES慢上好幾倍,無論是軟體還是硬體實作,速度一直是RSA的缺陷,是以一般隻用于少量資料的加密。RSA的速度是對應同樣安全級别的對稱密碼算法的1/1000左右。
一般使用對稱算法來加密資料,然後用RSA來加密對稱密鑰,然後将用RSA加密的對稱密鑰和用對稱算法加密的消息發送出去。這樣一來對随機數的要求就更高了,尤其對産生對稱密碼的要求非常高,因為否則的話可以越過RSA來直接攻擊對稱密碼。
3、加密的系統不要具備解密的功能,否則 RSA 可能不太合适:
公鑰加密,私鑰解密。加密的系統和解密的系統分開部署,加密的系統不應該同時具備解密的功能,這樣即使黑客攻破了加密系統,他拿到的也隻是一堆無法破解的密文資料。否則的話,你就要考慮你的場景是否有必要用 RSA 了。
4、可以通過修改生成密鑰的長度來調整密文長度:
生成密文的長度等于密鑰長度。密鑰長度越大,生成密文的長度也就越大,加密的速度也就越慢,而密文也就越難被破解掉。我們必須通過定義密鑰的長度在”安全”和”加解密效率”之間做出一個平衡的選擇。
5、生成密文的長度和明文長度無關,但明文長度不能超過密鑰長度:
不管明文長度是多少,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 的異常)。
6、可以通過調整算法提供者來減小密文長度:
Java 預設的 RSA 實作 “RSA/None/PKCS1Padding” 要求最小密鑰長度為 512 位(否則會報 java.security.InvalidParameterException: RSA keys must be at least 512 bits long 異常),也就是說生成的密鑰、密文長度最小為 64 個位元組。如果你還嫌大,可以通過調整算法提供者來減小密文長度:
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC");
keyGen.initialize(128);
複制
如此這般得到的密文長度為 128 位(16 個位元組)。但是這麼幹之前請先回顧一下上面第 4 點所述。
7、 byte[].toString() 傳回的實際上是記憶體位址,不是将數組的實際内容轉換為 String:
Java 中數組的 toString() 方法傳回的并非數組内容,它傳回的實際上是數組存儲元素的類型以及數組在記憶體的位置的一個辨別。如果我們對密鑰以 byte[].toString() 進行持久化存儲或者和其他一些字元串如 json 傳輸,那麼密鑰的解密者得到的将隻是一串毫無意義的字元,當他解碼的時候很可能會遇到 “javax.crypto.BadPaddingException” 異常。
8、字元串用以儲存文本資訊,位元組數組用以儲存二進制資料:
java.lang.String 儲存明文,byte 數組儲存二進制密文,在 java.lang.String 和 byte[] 之間不應該具備互相轉換。如果你确實必須得使用 java.lang.String 來持有這些二進制資料的話,最安全的方式是使用 Base64(推薦 Apache 的 commons-codec 庫的 org.apache.commons.codec.binary.Base64):
// use String to hold cipher binary data
Base64 base64 = new Base64();
String cipherTextBase64 = base64.encodeToString(cipherText);
// get cipher binary data back from String
byte[] cipherTextArray = base64.decode(cipherTextBase64);
複制
9、每次生成的密文都不一緻證明你選用的加密算法很安全:
一個優秀的加密必須每次生成的密文都不一緻,即使每次你的明文一樣、使用同一個公鑰。因為這樣才能把明文資訊更安全地隐藏起來。
Java 預設的 RSA 實作是 “RSA/None/PKCS1Padding”(比如 Cipher cipher = Cipher.getInstance(“RSA”);句,這個 Cipher 生成的密文總是不一緻的),Bouncy Castle 的預設 RSA 實作是 “RSA/None/NoPadding”。
為什麼 Java 預設的 RSA 實作每次生成的密文都不一緻呢,即使每次使用同一個明文、同一個公鑰?這是因為 RSA 的 PKCS #1 padding 方案在加密前對明文資訊進行了随機數填充。
你可以使用以下辦法讓同一個明文、同一個公鑰每次生成同一個密文,但是你必須意識到你這麼做付出的代價是什麼。比如,你可能使用 RSA 來加密傳輸,但是由于你的同一明文每次生成的同一密文,攻擊者能夠據此識别到同一個資訊都是何時被發送。
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final Cipher cipher = Cipher.getInstance("RSA/None/NoPadding", "BC");
複制
10、Cipher 是有狀态的,而且是線程不安全的:
javax.crypto.Cipher 是有狀态的,不要把 Cipher 當做一個靜态變量,除非你的程式是單線程的,也就是說你能夠保證同一時刻隻有一個線程在調用 Cipher。否則可能會遇到 java.lang.ArrayIndexOutOfBoundsException: too much data for RSA block 異常。遇見這個異常,你需要先确定你給 Cipher 加密的明文(或者需要解密的密文)是否過長;排除掉明文(或者密文)過長的情況,你需要考慮是不是你的 Cipher 線程不安全了。
11、對于加密後的資料,如果要列印出來,必須以十六進制或者BCD碼的形式列印:
不能new String(byte[])後,再從這個String裡getbytes(),也不要用base64,不然會破壞原資料。
比如:
byte[ ] bytes=new byte[ ]{108, -56, 111, 34, -67};
byte[ ] newBytes=new String(bytes).getBytes();
StringBuffer sb=new StringBuffer();
for(int i=0; i<newBytes.length; i++){
sb.append(newBytes[i]+"|");
}
System.out.println(sb.toString());
複制
将一個byte數組new String後再getbytes出來後,看看運作結果:

最後一個byte由-67變為了63。
參考部落格:
https://blog.csdn.net/dbs1215/article/details/48953589
https://blog.csdn.net/defonds/article/details/42775183#commentBox
釋出者:全棧程式員棧長,轉轉請注明出處:https://javaforall.cn/100045.html原文連結: