作者: zyl910
一、緣由
RSA是一種常用的非對稱加密算法。所以有時需要在不用編程語言中分別使用RSA的加密、解密。例如用Java做后台服務端,用C#開發桌面的客戶端軟件時。
由於 .Net、Java 的RSA類庫存在很多細節區別,尤其是它們支持的密鑰格式不同。導致容易出現“我加密的數據對方不能解密,對方加密的數據我不能解密,但是自身是可以正常加密解密”等情況。
雖然網上已經有很多文章討論 .Net與Java互通的RSA加解密,但是存在不夠全面、需要第三方dll、方案復雜 等問題。
於是我仔細研究了這一課題,得到了一些穩定可靠的代碼。現在將研究成果分享給大家。
二、密鑰
2.1 RSA密鑰文件格式介紹
要保證 .Net與Java 兩端均能正常的加解密,其中的重中之重就是確立一種密鑰文件格式,使 .Net與Java 兩端均能正確的加載密鑰。
.Net與Java內置類庫對密鑰文件格式的支持情況——
.Net
: 支持xml格式的密鑰文件。Java
: 沒有直接提供對密鑰文件的支持,僅提供了 PKCS#8、X.509 等編碼的密鑰數據的解析類。
2.1.1 技術細節——密鑰文件為什么這么復雜
看到 PKCS#8、X.509,大家是否有些頭暈了?
其實RSA的密鑰文件不止這2種,還有許多種存儲格式。可參考 蔣國綱《那些證書相關的玩意兒(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》。
為什么RSA密鑰文件這么復雜,這是因為密鑰文件需存儲多個數值。具體來說,RSA加解密中有5個重要的數字 p,q,n(Modulus),e(Exponent),d。然后公鑰與私鑰分別要存儲不同的值——
- 公鑰:需存儲 n、e。
- 私鑰:需存儲 n、d。而對於常用的X.509等編碼的私鑰文件中,其不僅存儲了 n、e、d、p、q,還存儲了 d mod (p-1)、d mod (q-1)、(inverse of q) mod p 等用於簡化、校驗加密的值。
所以我們會發現私鑰文件的字節數,一般比公鑰文件大一些。
為了統一密鑰文件格式,我們不得不編寫密鑰解析代碼,這需要理解rsa的p、q、n、e、d 具體含義與用法。學習難度較高,需要一定時間仔細研讀。
所以我便封裝了一些穩定、可靠的函數來處理這些內容。使下次可以直接用這些函數,不用再次費神處理這些復雜的技術細節。
若想支持絕大多數的密鑰文件格式,推薦使用 OpenSSL庫。它支持 .Net與Java。
可是,該庫比較龐大,項目依賴多會導致部署麻煩,不適合小型程序。所以我們還是選擇一種格式比較好。
2.2 確立密鑰文件格式
我挑選密鑰文件格式有2個條件——
- 文本格式。這樣用記事本打開密鑰文件,能夠方便的復制粘貼,且能作為程序中的字符串常量。使用靈活,方便測試等。
- 易於生成。不必編寫、運行代碼來生成,而是能夠通過多種辦法來生成密鑰對。既可以命令行生成,又可以通過圖形界面工具點擊生成。
所以最終選擇了 PEM(Privacy Enhanced Mail)格式的密鑰文件。用記事本打開可看到文本內容,其以"-----BEGIN..."開頭,以"-----END..."結尾,內容是BASE64編碼。
隨后對於具體的公鑰、私鑰的編碼格式,選擇了 PKCS#8 與 X.509,具體情況是——
- 公鑰:X.509 pem。Java類為 X509EncodedKeySpec 。
- 私鑰:PKCS#8 pem。Java類為 PKCS8EncodedKeySpec 。
2.3 生成密鑰
首先,可使用代碼來生成密鑰對,.Net、Java的類庫有完善的支持。該辦法適合於自己生成、管理密鑰的項目。但對於一些小型項目來說,該辦法比較復雜,不太實用。
其次,可以使用 OpenSSL 等命令行工具來生成密鑰。需要花點時間來學習命令行,並且需要安裝相應工具,稍微有點麻煩。
其實還有第三種方法,就是用在線工具來生成密鑰。因為我們用的是PEM格式的密鑰,該格式簡單,很多在線工具都支持。
例如 http://web.chacuo.net/netrsakeypair
用法——
- 選擇“生成密鑰位數”。直接使用默認的“2048位”就行,因為2048位是目前主流的密鑰位數,且.Net、Java均支持該長度。
- 選擇“密鑰格式”。直接使用默認的“PKCS#8”就行,因為我們也是采用這種格式。
- 填寫“證書密碼”。一般不用填寫。
- 點擊“生成密鑰對(RSA)”。隨后下面的兩個文本框分別會出現公鑰與私鑰,便可復制粘貼進行保存了。
2.3.1 本文范例用的密鑰
公鑰(public1.pem)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAywl5THDMsLUbzYX66YGp
Mr9AaiX6NNHp4gOQMa0BDM125ZftY/YL7ZJT9TgnVegK/vVSJn2PoGTw+x0OMx86
nCXOxX7h7xRt6oVRq3ekN36kBjGm56MFbYpAaLg0LLfPQcZME1g6T8CGCGpSZR90
bwqBh56uRFKa5ptJwLCloCc9fvW4uP6M/CcaRcpRcF0f4ofV/Urvq2l4Id+XxQyr
WX1JgR9mo6dvUaaX9osjZW615t6PlyoewkUUfv5rNTh7wjIZzKLl+pD8YCheZ7aJ
PlJWaIuwSENgVEYEbXcOyCbr2HqWA7EKA5+QxSaVy5z7q5BDpEz8ky3QxRfj+EDJ
VQIDAQAB
-----END PUBLIC KEY-----
私鑰(private1.pem)
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLCXlMcMywtRvN
hfrpgakyv0BqJfo00eniA5AxrQEMzXbll+1j9gvtklP1OCdV6Ar+9VImfY+gZPD7
HQ4zHzqcJc7FfuHvFG3qhVGrd6Q3fqQGMabnowVtikBouDQst89BxkwTWDpPwIYI
alJlH3RvCoGHnq5EUprmm0nAsKWgJz1+9bi4/oz8JxpFylFwXR/ih9X9Su+raXgh
35fFDKtZfUmBH2ajp29Rppf2iyNlbrXm3o+XKh7CRRR+/ms1OHvCMhnMouX6kPxg
KF5ntok+UlZoi7BIQ2BURgRtdw7IJuvYepYDsQoDn5DFJpXLnPurkEOkTPyTLdDF
F+P4QMlVAgMBAAECggEAIbtJM7Hpz9HG9LY1oWWxPoUXpor4rp3RRYNiCV68tevM
vQgooFrYUHfnCu5xWoxah1EqfMqPeg5LGu0Q1t1xV0/Qsm8KCjZSrIvJrbsKxU18
4qqNGB61YCV/3eX8hRFklYDkUrJtvaI2ol9HoRVAutH8AxQRz7gJlBZogmLWoWyX
r5CwPat/6n7mw//LtSblP9A10I8X+1G+9LFF48TKIZWvxkCkiLWiFwqQgbmfVdw8
vtCyMHLb62C3o6qTEjOYGD3xlE5kGPO7AovUihC8e/E5CaR840p+5j12qy62VbG6
7d0KFHIwAF4njhQA1wEWn+C+27lzE1Ps9eb3xlQdYQKBgQDuHCd0UewvL9YF6TYA
y2IuYtwDBlF2TZpJ5+y396ncHhdL90vAeIoDcBlK8zwBuH1M7Ewv3NlcNB1zlT95
itltPqdDkdl4TXboDTWrIhDD5RqiowrLTRSlO1hdZOw9ya88lxLYsUvMrNZzR3zW
T355YzqIC9JQYRu/O7+nysPiGwKBgQDaSrhz13c+PrUeExE34y3cdlN5aZkn3Rw/
MRpQWpV0+9NuTdBizENZ5uW3kCTI5+vk3OmgmCa2Lq48LZjKPa7BffIPK406V1Vs
xSZyzeTRRtaG7+Is1uTyASAimQ/0EIX3HjtZmHSPGeKyvYhKy0M+W1j1zPN1iP6w
Dy1nUMI5TwKBgQDQ5EQ8yQ4yi33w65rj8Ynt9e7cfHOFHSmpgt1qu8z5/jAkBg0g
Ct/Riku2NFPFkqviiz9/kfni6RmZaCsqnwSG0bt+DPtDjnottEEMJLOemGTYn779
gl8FYl3weXTD9CdXOZZgIpLEOjFdKy86+LyVE9equOxGdhsYlvtZ4godVwKBgQCa
ndpQkwlvGVOIXdEQWOWfBmDR2q4UwlTDnbAZwk+icMytkIhNsojyIM4NWxfzBfLc
RG1mxt6EpEPddB6JAW/Ktb7CaAK8lCd5x5sYLiYo5ZgGM9tsDzpS/+EXIHtgUGPT
SaKYL5g/1AHywLTM5XRXsrQsRmMbmVFsuxNZ3qXzmQKBgQDX9MkY7vDz5n27XtIQ
S65K5Wsmoqx5T+xhxQ9pRSbHm9t7cAO0We5sMLsAIjt1vKNBSeYLgxtqdEUcylb5
bZNVj5+qQFzcBh9yl7HtcAe3IkBvkrTAkonHN7gNqXKFUGlFkEFTBJm8IiSeUB9E
J99XfDatcok6GddO++ZMowAAJQ==
-----END PRIVATE KEY-----
2.4 Java加載密鑰
2.4.1 PEM解包
對於解析密鑰文件,第一個重要步驟就是進行PEM解包。這是因為PEM文件是以“-----BEGIN”開頭、“-----END”結尾的,而實際的密鑰數據是以BASE64編碼的形式給放在中間的。
由於Java沒有直接提供對密鑰文件的支持,僅提供了 PKCS#8、X.509 等編碼的密鑰數據的解析類。於是需要我們自己來做PEM解包。
我觀察了網上的PEM解包的源碼,發現它們一般是用字符串數組存儲“-----BEGIN”的各種模式,然后根據該數組查找字符串來來定位數據的。但該辦法並不穩定,容易遇到問題——
- BEGIN后面的文本內容不規范。例如有寫成“-----BEGIN PUBLIC KEY”開頭的,有寫成“-----BEGIN RSA PUBLIC KEY”開頭的,還有其他各種五花八門的模式。
- BEGIN(或END)前后的減號(
-
)長度不定。不同工具生成的PEM文件中,減號(-
)長度是不同的。 - 有時中間會有多余的空格等空白字符。
於是我寫了個狀態機算法來解析PEM數據。這樣便能處理各種意外,提高穩定性。
另外,該算法還增加自動判斷是公鑰還是私鑰的功能。由於Java函數不允許返回多個值,所以用了一個Map來傳遞多余的返回值。
/** 用途文本. 如“BEGIN PUBLIC KEY”中的“PUBLIC KEY”. */
public final static String PURPOSE_TEXT = "PURPOSE_TEXT";
/** 用途代碼. R私鑰, U公鑰. */
public final static String PURPOSE_CODE = "PURPOSE_CODE";
/** PEM解包.
*
* <p>從PEM密鑰數據中解包得到純密鑰數據. 即去掉BEGIN/END行,並作BASE64解碼. 若沒有BEGIN/END, 則直接做BASE64解碼.</p>
*
* @param data 源數據.
* @param otherresult 其他返回值. 支持 PURPOSE_TEXT, PURPOSE_CODE。
* @return 返回解包后的純密鑰數據.
*/
public static byte[] PemUnpack(String data, Map<String, String> otherresult) {
byte[] rt = null;
final String SIGN_BEGIN = "-BEGIN";
final String SIGN_END = "-END";
int datelen = data.length();
String purposetext = "";
String purposecode = "";
if (null!=otherresult) {
purposetext = otherresult.get(PURPOSE_TEXT);
purposecode = otherresult.get(PURPOSE_CODE);
if (null==purposetext) purposetext= "";
if (null==purposecode) purposecode= "";
}
// find begin.
int bodyPos = 0; // 主體內容開始的地方.
int beginPos = data.indexOf(SIGN_BEGIN);
if (beginPos>=0) {
// 向后查找換行符后的首個字節.
boolean isFound = false;
boolean hadNewline = false; // 已遇到過換行符號.
boolean hyphenHad = false; // 已遇到過“-”符號.
boolean hyphenDone = false; // 已成功獲取了右側“-”的范圍.
int p = beginPos + SIGN_BEGIN.length();
int hyphenStart = p; // 右側“-”的開始位置.
int hyphenEnd = hyphenStart; // 右側“-”的結束位置. 即最后一個“-”字符的位置+1.
while(p<datelen) {
char ch = data.charAt(p);
// 查找右側“-”的范圍.
if (!hyphenDone) {
if (ch=='-') {
if (!hyphenHad) {
hyphenHad = true;
hyphenStart = p;
hyphenEnd = hyphenStart;
}
} else {
if (hyphenHad) { // 無需“&& !hyphenDone”,因為外層判斷了.
hyphenDone = true;
hyphenEnd = p;
}
}
}
// 向后查找換行符后的首個字節.
if (ch=='\n' || ch=='\r') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyPos = p;
isFound = true;
break;
}
}
// next.
++p;
}
// purposetext
if (hyphenDone && null!=otherresult) {
purposetext = data.substring(beginPos + SIGN_BEGIN.length(), hyphenStart).trim();
String purposetextUp = purposetext.toUpperCase();
if (purposetextUp.indexOf("PRIVATE")>=0) {
purposecode = "R";
} else if (purposetextUp.indexOf("PUBLIC")>=0) {
purposecode = "U";
}
otherresult.put(PURPOSE_TEXT, purposetext);
otherresult.put(PURPOSE_CODE, purposecode);
}
// bodyPos.
if (isFound) {
//OK.
} else if (hyphenDone) {
// 以右側右側“-”的結束位置作為主體開始.
bodyPos = hyphenEnd;
} else {
// 找不到結束位置,只能退出.
return rt;
}
}
// find end.
int bodyEnd = datelen; // 主體內容的結束位置. 即最后一個字符的位置+1.
int endPos = data.indexOf(SIGN_END, bodyPos);
if (endPos>=0) {
// 向前查找換行符前的首個字節.
boolean isFound = false;
boolean hadNewline = false;
int p = endPos-1;
while(p >= bodyPos) {
char ch = data.charAt(p);
if (ch=='\n' || ch=='\r') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyEnd = p+1;
break;
}
}
// next.
--p;
}
if (!isFound) {
// 忽略.
}
}
// get body.
if (bodyPos>=bodyEnd) {
return rt;
}
String body = data.substring(bodyPos, bodyEnd).trim();
// Decode BASE64.
rt = Base64.decode(body.getBytes());
return rt;
}
2.4.2 加載公鑰
PemUnpack解出純密鑰數據后,便可分別加載公鑰與私鑰了。
由於Java提供了X509EncodedKeySpec,加載公鑰是比較簡單的。
下面代碼中的strDataKey為PEM文本內容,最后的 key 就是公鑰對象。
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
2.4.3 加載私鑰
由於Java提供了PKCS8EncodedKeySpec,加載私鑰是比較簡單的。
下面代碼中的strDataKey為PEM文本內容,最后的 key就是私鑰對象。
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
2.4.4 判斷密鑰位數
密鑰位數是一個很重要的數值,很多地方都要用到。可是Java沒有簡單的提供該屬性,而是需要一些步驟來得到,且公鑰、私鑰得使用不同的類。
- 調用 KeyFactory.getKeySpec 方法,傳遞EncodedKeySpec(公鑰為X509EncodedKeySpec,私鑰為PKCS8EncodedKeySpec),獲取 KeySpec(公鑰為RSAPublicKeySpec,私鑰為RSAPrivateKeySpec)。
- 隨后調用 KeySpec對象的 getModulus 方法獲取 Modulus(即n)。
- 獲取 Modulus(即n)的位數,它就是密鑰位數。
范例代碼如下——
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
int keysize;
// 公鑰.
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
// 私鑰.
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
2.4.4 小結
剛才講解了加載密鑰過程中的各個關鍵步驟,現在來將它們組合起來吧。演示一下完整的密鑰加載過程。
參數說明——
fileKey
: 密鑰文件.
String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
//out.println(bytesKey);
// key.
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
int keysize;
if ("R".equals(purposecode)) {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
} else {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
}
System.out.println(String.format("keysize: %d", keysize));
System.out.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
System.out.println(String.format("key.getFormat: %s", key.getFormat()));
其中的 ZlRsaUtil.fileLoadBytes 是一個加載文件的函數。嚴格來說,是加載文件的二進制數據。因為PEM文件是純ASCII的,故可以簡單的通過 new String
的方式轉為字符串。
/**
* RSA .
*/
public final static String RSA = "RSA";
/** 加載文件中的所有字節.
*
* @param filename 文件名.
* @return 返回文件內容的字節數組.
* @throws IOException IO異常.
*/
public static byte[] fileLoadBytes(String filename) throws IOException {
byte[] rt = null;
File file = new File(filename);
long fileSize = file.length();
if (fileSize > Integer.MAX_VALUE) {
throw new IOException(filename + " file too big...");
}
FileInputStream fi = new FileInputStream(filename);
try {
rt = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < rt.length
&& (numRead = fi.read(rt, offset, rt.length - offset)) >= 0) {
offset += numRead;
}
// 確保所有數據均被讀取
if (offset != rt.length) {
throw new IOException("Could not completely read file " + file.getName());
}
}finally{
try {
fi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return rt;
}
2.5 .Net加載密鑰
2.5.1 PEM解包
.Net里僅提供對Xml密鑰文件的支持,所以我們得自己編寫PEM的解包代碼。
同樣是因為網上范例代碼考慮的不周全,於是我寫了個狀態機算法來解析PEM數據。能處理各種意外,提高了穩定性。
/// <summary>
/// PEM解包.
/// </summary>
/// <para>從PEM密鑰數據中解包得到純密鑰數據. 即去掉BEGIN/END行,並作BASE64解碼. 若沒有BEGIN/END, 則直接做BASE64解碼.</para>
/// <param name="data">源數據.</param>
/// <param name="purposetext">用途文本. 如返回“BEGIN PUBLIC KEY”中的“PUBLIC KEY”.</param>
/// <param name="purposecode">用途代碼. R私鑰, U公鑰. 若無法識別,便保持原值.</param>
/// <returns>返回解包后的純密鑰數據.</returns>
/// <exception cref="System.ArgumentNullException">data is empty, or data body is empty.</exception>
/// <exception cref="System.FormatException">data body is not BASE64.</exception>
public static byte[] PemUnpack(String data, ref string purposetext, ref char purposecode) {
byte[] rt = null;
const string SIGN_BEGIN = "-BEGIN";
const string SIGN_END = "-END";
if (String.IsNullOrEmpty(data)) throw new ArgumentNullException("data", "data is empty!");
int datelen = data.Length;
// find begin.
int bodyPos = 0; // 主體內容開始的地方.
int beginPos = data.IndexOf(SIGN_BEGIN, StringComparison.OrdinalIgnoreCase);
if (beginPos >= 0) {
// 向后查找換行符后的首個字節.
bool isFound = false;
bool hadNewline = false; // 已遇到過換行符號.
bool hyphenHad = false; // 已遇到過“-”符號.
bool hyphenDone = false; // 已成功獲取了右側“-”的范圍.
int p = beginPos + SIGN_BEGIN.Length;
int hyphenStart = p; // 右側“-”的開始位置.
int hyphenEnd = hyphenStart; // 右側“-”的結束位置. 即最后一個“-”字符的位置+1.
while (p < datelen) {
char ch = data[p];
// 查找右側“-”的范圍.
if (!hyphenDone) {
if (ch == '-') {
if (!hyphenHad) {
hyphenHad = true;
hyphenStart = p;
hyphenEnd = hyphenStart;
}
} else {
if (hyphenHad) { // 無需“&& !hyphenDone”,因為外層判斷了.
hyphenDone = true;
hyphenEnd = p;
}
}
}
// 向后查找換行符后的首個字節.
if (ch == '\n' || ch == '\r') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyPos = p;
isFound = true;
break;
}
}
// next.
++p;
}
// purposetext
if (hyphenDone) {
int start = beginPos + SIGN_BEGIN.Length;
purposetext = data.Substring(start, hyphenStart - start).Trim();
string purposetextUp = purposetext.ToUpperInvariant();
if (purposetextUp.IndexOf("PRIVATE") >= 0) {
purposecode = 'R';
} else if (purposetextUp.IndexOf("PUBLIC") >= 0) {
purposecode = 'U';
}
}
// bodyPos.
if (isFound) {
//OK.
} else if (hyphenDone) {
// 以右側右側“-”的結束位置作為主體開始.
bodyPos = hyphenEnd;
} else {
// 找不到結束位置,只能退出.
return rt;
}
}
// find end.
int bodyEnd = datelen; // 主體內容的結束位置. 即最后一個字符的位置+1.
int endPos = data.IndexOf(SIGN_END, bodyPos);
if (endPos >= 0) {
// 向前查找換行符前的首個字節.
bool isFound = false;
bool hadNewline = false;
int p = endPos - 1;
while (p >= bodyPos) {
char ch = data[p];
if (ch == '\n' || ch == '\r') {
hadNewline = true;
} else {
if (hadNewline) {
// 找到了.
bodyEnd = p + 1;
break;
}
}
// next.
--p;
}
if (!isFound) {
// 忽略.
}
}
// get body.
if (bodyPos >= bodyEnd) {
return rt;
}
string body = data.Substring(bodyPos, bodyEnd - bodyPos).Trim();
// Decode BASE64.
if (String.IsNullOrEmpty(body)) throw new ArgumentNullException("data", "data body is empty!");
rt = Convert.FromBase64String(body);
return rt;
}
2.5.2 加載公鑰
由於.Net平台沒有提供 X.509 的解碼類,故需要自己編寫。
我參考網上代碼,寫了一個公鑰的解碼函數。
/// <summary>
/// 根據PEM純密鑰數據,獲取公鑰的RSA加解密對象.
/// </summary>
/// <param name="pubcdata">公鑰數據</param>
/// <returns>返回公鑰的RSA加解密對象.</returns>
public static RSACryptoServiceProvider PemDecodePublicKey(byte[] pubcdata) {
byte[] SeqOID = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
MemoryStream ms = new MemoryStream(pubcdata);
BinaryReader reader = new BinaryReader(ms);
if (reader.ReadByte() == 0x30)
ReadASNLength(reader); //skip the size
else
return null;
int identifierSize = 0; //total length of Object Identifier section
if (reader.ReadByte() == 0x30)
identifierSize = ReadASNLength(reader);
else
return null;
if (reader.ReadByte() == 0x06) { //is the next element an object identifier?
int oidLength = ReadASNLength(reader);
byte[] oidBytes = new byte[oidLength];
reader.Read(oidBytes, 0, oidBytes.Length);
if (!SequenceEqualByte(oidBytes, SeqOID)) //is the object identifier rsaEncryption PKCS#1?
return null;
int remainingBytes = identifierSize - 2 - oidBytes.Length;
reader.ReadBytes(remainingBytes);
}
if (reader.ReadByte() == 0x03) { //is the next element a bit string?
ReadASNLength(reader); //skip the size
reader.ReadByte(); //skip unused bits indicator
if (reader.ReadByte() == 0x30) {
ReadASNLength(reader); //skip the size
if (reader.ReadByte() == 0x02) { //is it an integer?
int modulusSize = ReadASNLength(reader);
byte[] modulus = new byte[modulusSize];
reader.Read(modulus, 0, modulus.Length);
if (modulus[0] == 0x00) {//strip off the first byte if it's 0
byte[] tempModulus = new byte[modulus.Length - 1];
Array.Copy(modulus, 1, tempModulus, 0, modulus.Length - 1);
modulus = tempModulus;
}
if (reader.ReadByte() == 0x02) { //is it an integer?
int exponentSize = ReadASNLength(reader);
byte[] exponent = new byte[exponentSize];
reader.Read(exponent, 0, exponent.Length);
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
RSAParameters RSAKeyInfo = new RSAParameters();
RSAKeyInfo.Modulus = modulus;
RSAKeyInfo.Exponent = exponent;
RSA.ImportParameters(RSAKeyInfo);
return RSA;
}
}
}
}
return null;
}
/// <summary>
/// Read ASN Length.
/// </summary>
/// <param name="reader">reader</param>
/// <returns>Return ASN Length.</returns>
private static int ReadASNLength(BinaryReader reader) {
//Note: this method only reads lengths up to 4 bytes long as
//this is satisfactory for the majority of situations.
int length = reader.ReadByte();
if ((length & 0x00000080) == 0x00000080) { //is the length greater than 1 byte
int count = length & 0x0000000f;
byte[] lengthBytes = new byte[4];
reader.Read(lengthBytes, 4 - count, count);
Array.Reverse(lengthBytes); //
length = BitConverter.ToInt32(lengthBytes, 0);
}
return length;
}
/// <summary>
/// 字節數組內容是否相等.
/// </summary>
/// <param name="a">數組a</param>
/// <param name="b">數組b</param>
/// <returns>返回是否相等.</returns>
private static bool SequenceEqualByte(byte[] a, byte[] b) {
var len1 = a.Length;
var len2 = b.Length;
if (len1 != len2) {
return false;
}
for (var i = 0; i < len1; i++) {
if (a[i] != b[i])
return false;
}
return true;
}
2.5.3 加載私鑰
.Net平台也沒有提供 PKCS#8 的解碼類,也需要自己編寫。
我最初測試了很多網上的私鑰解碼代碼,均不能正常工作。直到后來查了 OpenSSL 的源碼,才找到了解決辦法。發現這是因為PKCS#8的私鑰數據,其實還嵌套了一層X.509編碼,故得按順序分別進行解碼。
/// <summary>
/// 解碼 PKCS#8 編碼的私鑰,獲取私鑰的RSA加解密對象.
/// </summary>
/// <param name="privkey">私鑰數據。</param>
/// <returns>返回私鑰的RSA加解密對象. 失敗時返回null.</returns>
public static RSACryptoServiceProvider PemDecodePkcs8PrivateKey(byte[] pkcs8) {
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
// this byte[] includes the sequence byte and terminal encoded null
byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
byte[] seq = new byte[15];
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
MemoryStream mem = new MemoryStream(pkcs8);
int lenstream = (int)mem.Length;
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
byte bt = 0;
ushort twobytes = 0;
try {
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
bt = binr.ReadByte();
if (bt != 0x02)
return null;
twobytes = binr.ReadUInt16();
if (twobytes != 0x0001)
return null;
seq = binr.ReadBytes(15); //read the Sequence OID
if (!SequenceEqualByte(seq, SeqOID)) //make sure Sequence for OID is correct
return null;
bt = binr.ReadByte();
if (bt != 0x04) //expect an Octet string
return null;
bt = binr.ReadByte(); //read next byte, or next 2 bytes is 0x81 or 0x82; otherwise bt is the byte count
if (bt == 0x81)
binr.ReadByte();
else
if (bt == 0x82)
binr.ReadUInt16();
//------ at this stage, the remaining sequence should be the RSA private key
byte[] rsaprivkey = binr.ReadBytes((int)(lenstream - mem.Position));
RSACryptoServiceProvider rsacsp = PemDecodeX509PrivateKey(rsaprivkey);
return rsacsp;
} finally { binr.Close(); }
}
/// <summary>
/// 解碼 X.509 編碼的私鑰,獲取私鑰的RSA加解密對象.
/// </summary>
/// <param name="privkey">私鑰數據。</param>
/// <returns>返回私鑰的RSA加解密對象. 失敗時返回null.</returns>
public static RSACryptoServiceProvider PemDecodeX509PrivateKey(byte[] privkey)
{
byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
// --------- Set up stream to decode the asn.1 encoded RSA private key ------
MemoryStream mem = new MemoryStream(privkey);
BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading
byte bt = 0;
ushort twobytes = 0;
int elems = 0;
try
{
twobytes = binr.ReadUInt16();
if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
binr.ReadByte(); //advance 1 byte
else if (twobytes == 0x8230)
binr.ReadInt16(); //advance 2 bytes
else
return null;
twobytes = binr.ReadUInt16();
if (twobytes != 0x0102) //version number
return null;
bt = binr.ReadByte();
if (bt != 0x00)
return null;
//------ all private key components are Integer sequences ----
elems = GetIntegerSize(binr);
MODULUS = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
E = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
D = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
P = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
Q = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
DP = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
DQ = binr.ReadBytes(elems);
elems = GetIntegerSize(binr);
IQ = binr.ReadBytes(elems);
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
CspParameters CspParameters = new CspParameters();
CspParameters.Flags = CspProviderFlags.UseMachineKeyStore;
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(1024, CspParameters);
RSAParameters RSAparams = new RSAParameters();
RSAparams.Modulus = MODULUS;
RSAparams.Exponent = E;
RSAparams.D = D;
RSAparams.P = P;
RSAparams.Q = Q;
RSAparams.DP = DP;
RSAparams.DQ = DQ;
RSAparams.InverseQ = IQ;
RSA.ImportParameters(RSAparams);
return RSA;
}
finally
{
binr.Close();
}
}
/// <summary>
/// 取得整數大小.
/// </summary>
/// <param name="binr">BinaryReader</param>
/// <returns>返回整數大小.</returns>
private static int GetIntegerSize(BinaryReader binr)
{
byte bt = 0;
byte lowbyte = 0x00;
byte highbyte = 0x00;
int count = 0;
bt = binr.ReadByte();
if (bt != 0x02) //expect integer
return 0;
bt = binr.ReadByte();
if (bt == 0x81)
count = binr.ReadByte(); // data size in next byte
else
if (bt == 0x82)
{
highbyte = binr.ReadByte(); // data size in next 2 bytes
lowbyte = binr.ReadByte();
byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
count = BitConverter.ToInt32(modint, 0);
}
else
{
count = bt; // we already have the data size
}
while (binr.ReadByte() == 0x00)
{ //remove high order zeros in data
count -= 1;
}
binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte
return count;
}
2.5.4 判斷密鑰位數
在 .Net中,訪問 RSACryptoServiceProvider.KeySize 便可得到密鑰位數,非常簡單。
int keysize = rsa.KeySize;
2.5.4 小結
剛才講解了加載密鑰過程中的各個關鍵步驟,現在來將它們組合起來吧。演示一下完整的密鑰加載過程。
參數說明——
fileKey
: 密鑰文件.
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公鑰或無法判斷時, 均當成公鑰處理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
三、加解密
3.1 確立加密模式與填充方式
雖然都是RSA算法,但是若加密模式與填充方式不同的話,會導致加密結果不匹配。所以需要確定好 .Net與Java 均支持的方式。
加密模式一般有 ECB/CBC/CFB/OFB 這四種。對於RSA來說,ECB最簡單但安全性比較薄弱,而CBC等模式就很復雜且還需考慮IV(initialization vector,初始化向量)的管理。所以一般情況下可以用 ECB 模式,.Net與Java均支持它,且ECB是.Net的默認模式。
由於加密算法都是按塊來處理的,故理論上只有當明文長度正好是塊長度的倍數時才能進行加解密。但那樣太麻煩了,故有了填充方式的概念,即在明文后面填充一些數據,使其長度正好是塊的倍數。填充方式還有2個作用,一是能標記原始數據長度使解碼時自動去掉末尾的填充數據,二是能提高安全性。
.Net的RSA算法默認是使用PKCS#1填充方式的,故Java中可選擇 PKCS1Padding 填充方式。
現在算法已經確定了,Java中可定義這些常數。
/**
* RSA .
*/
public final static String RSA = "RSA";
/**
* 具體的 RSA 算法.
*/
public final static String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
3.2 分段加密
對於.Net、Java自帶的RSA庫來說,填充方式只是解決了“明文長度小於塊尺寸”的問題。而當明文長度大於塊尺寸時,便會拋出異常,常見的異常信息有——
// .Net
不正確的長度
// Java
javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes
此時便需要對數據進行分段加密。
3.2.1 塊尺寸的計算
密文的塊尺寸是很容易計算的,即“密鑰位數/8”。即把二進制長度轉為字節長度。
而明文的塊尺寸的計算就稍微麻煩了一點,與填充方式有關。因目前使用了PKCS#1填充方式,該方式需占用11個字節。於是塊尺寸為“密鑰位數/8 - 11”。
例如密鑰長度為2048位時——
- 密文的塊尺寸 = 密鑰位數/8 = 2048/8 = 256
- 明文的塊尺寸 = 密鑰位數/8 - 11 = 2048/8 - 11 = 256 - 11 = 245
即——
- 加密時:明文的塊為245字節,加密后輸出的密文塊為256字節。
- 解密時:密文的塊為256字節,解密后輸出的明文塊為245字節。
3.3 Java加解密
3.3.1 加密
/** RSA加密. 當數據較長時, 能自動分段加密.
*
* @param cipher 加解密服務提供者. 需是已初始化的, 即已經調了init的.
* @param keysize 密鑰長度. 例如2048位的RSA,傳2048 .
* @param data 欲加密的數據.
* @return 返回加密后的數據.
* @throws BadPaddingException On Cipher.doFinal
* @throws IllegalBlockSizeException On Cipher.doFinal
*/
public static byte[] encrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
byte[] cipherBytes = null;
int blockSize = keysize/8 - 11; // RSA加密時支持的最大字節數:證書位數/8 -11(比如:2048位的證書,支持的最大加密字節數:2048/8 - 11 = 245).
if (data.length <= blockSize) {
// 整個加密.
cipherBytes = cipher.doFinal(data);
} else {
// 分段加密.
int inputLen = data.length;
ByteArrayOutputStream ostm = new ByteArrayOutputStream();
try {
for(int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len>blockSize) len=blockSize;
byte[] cache = cipher.doFinal(data, offSet, len);
ostm.write(cache, 0, cache.length);
// next.
offSet += len;
}
cipherBytes = ostm.toByteArray();
}finally {
try {
ostm.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cipherBytes;
}
3.3.2 解密
/** RSA解密. 當數據較長時, 能自動分段解密.
*
* @param cipher 加解密服務提供者. 需是已初始化的, 即已經調了init的.
* @param keysize 密鑰長度. 例如2048位的RSA,傳2048 .
* @param data 欲解密的數據.
* @return 返回解密后的數據.
* @throws BadPaddingException On Cipher.doFinal
* @throws IllegalBlockSizeException On Cipher.doFinal
*/
public static byte[] decrypt(Cipher cipher, int keysize, byte[] data) throws IllegalBlockSizeException, BadPaddingException {
byte[] cipherBytes = null;
int blockSize = keysize/8;
if (data.length <= blockSize) {
// 整個加密.
cipherBytes = cipher.doFinal(data);
} else {
// 分段加密.
int inputLen = data.length;
ByteArrayOutputStream ostm = new ByteArrayOutputStream();
try {
for(int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len>blockSize) len=blockSize;
byte[] cache = cipher.doFinal(data, offSet, len);
ostm.write(cache, 0, cache.length);
// next.
offSet += len;
}
cipherBytes = ostm.toByteArray();
}finally {
try {
ostm.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return cipherBytes;
}
3.4 .Net加解密
3.3.1 加密
/// <summary>
/// RSA加密. 當數據較長時, 能自動分段加密.
/// </summary>
/// <param name="rsa">加解密服務提供者. 需是已初始化的.</param>
/// <param name="data">欲加密的數據.</param>
/// <returns>返回加密后的數據.</returns>
/// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
public static byte[] Encrypt(RSACryptoServiceProvider rsa, byte[] data) {
byte[] cipherBytes = null;
int keysize = rsa.KeySize;
int blockSize = keysize / 8 - 11; // RSA加密時支持的最大字節數:證書位數/8 -11(比如:2048位的證書,支持的最大加密字節數:2048/8 - 11 = 245).
if (data.Length <= blockSize) {
// 整個加密.
cipherBytes = rsa.Encrypt(data, false);
} else {
// 分段加密.
int inputLen = data.Length;
using (MemoryStream ostm = new MemoryStream()) {
for (int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len > blockSize) len = blockSize;
byte[] tmp = new byte[len];
Array.Copy(data, offSet, tmp, 0, len);
byte[] cache = rsa.Encrypt(tmp, false);
ostm.Write(cache, 0, cache.Length);
// next.
offSet += len;
}
ostm.Position = 0;
cipherBytes = ostm.ToArray();
}
}
return cipherBytes;
}
3.3.2 解密
/// <summary>
/// RSA解密. 當數據較長時, 能自動分段解密.
/// </summary>
/// <param name="rsa">加解密服務提供者. 需是已初始化的.</param>
/// <param name="data">欲解密的數據.</param>
/// <returns>返回解密后的數據.</returns>
/// <exception cref="System.Security.Cryptography.CryptographicException">On RSACryptoServiceProvider.Encrypt .</exception>
public static byte[] Decrypt(RSACryptoServiceProvider rsa, byte[] data) {
byte[] cipherBytes = null;
int keysize = rsa.KeySize;
int blockSize = keysize / 8;
if (data.Length <= blockSize) {
// 整個解密.
cipherBytes = rsa.Decrypt(data, false);
} else {
// 分段解密.
int inputLen = data.Length;
using (MemoryStream ostm = new MemoryStream()) {
for (int offSet = 0; inputLen - offSet > 0; ) {
int len = inputLen - offSet;
if (len > blockSize) len = blockSize;
byte[] tmp = new byte[len];
Array.Copy(data, offSet, tmp, 0, len);
byte[] cache = rsa.Decrypt(tmp, false);
ostm.Write(cache, 0, cache.Length);
// next.
offSet += len;
}
ostm.Position = 0;
cipherBytes = ostm.ToArray();
}
}
return cipherBytes;
}
四、測試驗證
4.1 編程測試
為了驗證.Net、Java的加解密代碼是否吻合,最好是寫一個測試程序進行驗證。然后便可分別測試——
- Java 端加密生成密文文件,隨后 Java 端讀取密文文件做解密。
- .Net 端加密生成密文文件,隨后 .Net 端讀取密文文件做解密。
- Java 端加密生成密文文件,隨后 .Net 端讀取密文文件做解密。
- .Net 端加密生成密文文件,隨后 Java 端讀取密文文件做解密。
這4種測試都通過后,便表示加解密沒問題。可穩定的運行在.Net、Java通訊的場景下。
4.1.1 命令行設計
為了方便多次重復測試,於是將該程序設計為命令行程序。這樣便能靈活的做各種測試。
該程序命名為 rsapemdemo。用法為 rsapemdemo [options] srcfile
。
命令的范例——
# 使用公鑰進行加密
rsapemdemo -e -l publickey.pem -o dstfile srcfile
# 使用私鑰進行解密
rsapemdemo -d -l privatekey.pem -o dstfile srcfile
參數說明——
-e:RSA加密,並進行BASE64編碼。因加密后得到的二進制數據不易查看、復制,故再做了一次BASE64編碼。
-d:BASE64解碼,並進行RSA解密。
-l [keyfile]:加載密鑰文件。
-o [outfile]:指定輸出文件。
srcfile:源文件名。
實際測試時所使用的命令行——
rsapemdemo -e -l "E:\rsapemdemo\data\public1.pem" -o "E:\rsapemdemo\data\src1_pub.log" "E:\rsapemdemo\data\src1.txt"
rsapemdemo -e -l "E:\rsapemdemo\data\private1.pem" -o "E:\rsapemdemo\data\src1_pri.log" "E:\rsapemdemo\data\src1.txt"
rsapemdemo -d -l "E:\rsapemdemo\data\public1.pem" -o "E:\rsapemdemo\data\src1_pri_d.log" "E:\rsapemdemo\data\src1_pri.log"
rsapemdemo -d -l "E:\rsapemdemo\data\private1.pem" -o "E:\rsapemdemo\data\src1_pub_d.log" "E:\rsapemdemo\data\src1_pub.log"
4.1.2 Java的測試辦法
在Eclipse中打開項目。
雙擊打開含有main函數的文件(RsaPemDemo.java),然后在源碼區域右擊鼠標,在彈出菜單中選擇“Debug As -> Debug Configurations”。
“Debug Configurations”對話框打開后,切換到“Arguments”頁,在“Program arguments”文本框中輸入命令行參數(不用輸入程序名,只需輸入后面的參數)。
隨后便可點擊“Debug”按鈕進行調試了。
4.1.3 .Net的測試辦法
在VS中打開項目。
點擊菜單欄的“項目->屬性”。
屬性對話框打開后,切換到“調試”頁,在“命令行參數”文本框中輸入命令行參數(不用輸入程序名,只需輸入后面的參數)。
隨后便可按F5調試了。
測試后發現——
- .NET 的RSA,僅支持公鑰加密、私鑰解密。若用私鑰加密,則仍是返回公鑰加密結果。若用公鑰解密,會出現
System.Security.Cryptography.CryptographicException: 不正確的項。
異常.
4.2 在線測試
除了自己編碼測試外,還可以使用RSA在線工具進行對比測試。檢測我們測試程序所生成的密文,是否能被在線工具解密,或者讓在線工具生成密文由我們程序進行解密。
例如可利用這個網站進行測試——
# 在線RSA公鑰加密解密、RSA public key encryption and decryption
http://tool.chacuo.net/cryptrsapubkey
# 在線RSA私鑰加密解密、RSA private key encryption and decryption
http://tool.chacuo.net/cryptrsaprikey
附錄、測試程序的主體源碼
附錄.1 Java版
package rsapemdemo;
import java.io.IOException;
import java.io.PrintStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
/** Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示項目,使用pem格式的密鑰文件).
*
* @author zyl910
* @since 2017-10-27
*
*/
public class RsaPemDemo {
/** 幫助文本. */
private static final String helpText = "Usage: rsapemdemo [options] srcfile\n\nFor example:\n\n # encode by public key\n rsapemdemo -e -l publickey.pem -o dstfile srcfile\n\n # decode by private key\n rsapemdemo -d -l privatekey.pem -o dstfile srcfile\n\nThe options:\n\n -e RSA encryption and BASE64 encode.\n -d BASE64 decode and RSA decryption.\n -l [keyfile] Load key file.\n -o [outfile] out file.\n";
/** 是否為空.
*
* @param str 字符串.
* @return 如果字符串為null或空串,則返回true,否則返回false.
*/
private static boolean isEmpty(String str) {
return null==str || str.length()<=0;
}
/** 運行.
*
* @param export 文本打印流.
* @param args 參數.
* @return 程序退出碼.
*/
public void run(PrintStream export, String[] args) {
boolean showhelp = true;
// args
String state = null; // 狀態.
boolean isEncode = false;
boolean isDecode = false;
String fileKey = null;
String fileOut = null;
String fileSrc = null;
int keysize = 0; // RSA密鑰位數. 0表示自動獲取.
for(String s: args) {
if ("-e".equalsIgnoreCase(s)) {
isEncode = true;
} else if ("-d".equalsIgnoreCase(s)) {
isDecode = true;
} else if ("-l".equalsIgnoreCase(s)) {
state = "l";
} else if ("-o".equalsIgnoreCase(s)) {
state = "o";
} else {
if ("l".equalsIgnoreCase(state)) {
fileKey = s;
state = null;
} else if ("o".equalsIgnoreCase(state)) {
fileOut = s;
state = null;
} else {
fileSrc = s;
}
}
}
try{
if (isEmpty(fileKey)) {
export.println("No key file! Command need add `-l [keyfile]`.");
} else if (isEmpty(fileOut)) {
export.println("No out file! Command need add `-o [outfile]`.");
} else if (isEmpty(fileSrc)) {
export.println("No src file! Command need add `[srcfile]`.");
} else if (isEncode!=false && isDecode!=false) {
export.println("No set Encode/Encode! Command need add `-e`/`-d`.");
} else if (isEncode) {
showhelp = false;
doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
} else if (isDecode) {
showhelp = false;
doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
}
} catch (Exception e) {
e.printStackTrace(export);
}
// do.
if (showhelp) {
export.println(helpText);
}
}
/** 進行加密.
*
* @param export 文本打印流.
* @param keysize 密鑰位數. 為0表示自動獲取.
* @param fileKey 密鑰文件.
* @param fileOut 輸出文件.
* @param fileSrc 源文件.
* @param exargs 擴展參數.
* @throws IOException
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
private void doEncode(PrintStream export, int keysize, String fileKey, String fileOut,
String fileSrc, Map<String, ?> exargs) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] bytesSrc = ZlRsaUtil.fileLoadBytes(fileSrc);
String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
//out.println(bytesKey);
// key.
KeyFactory kf = KeyFactory.getInstance(ZlRsaUtil.RSA);
Key key= null;
//boolean isPrivate = false;
if ("R".equals(purposecode)) {
//isPrivate = true;
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
} else {
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
}
export.println(String.format("keysize: %d", keysize));
export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
export.println(String.format("key.getFormat: %s", key.getFormat()));
// encrypt.
Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherBytes = ZlRsaUtil.encrypt(cipher, keysize, bytesSrc);
byte[] cipherBase64 = Base64.encode(cipherBytes);
ZlRsaUtil.fileSaveBytes(fileOut, cipherBase64, 0, cipherBase64.length);
export.println(String.format("%s save done.", fileOut));
}
/** 進行解密.
*
* @param export 文本打印流.
* @param keysize 密鑰位數. 為0表示自動獲取.
* @param fileKey 密鑰文件.
* @param fileOut 輸出文件.
* @param fileSrc 源文件.
* @param exargs 擴展參數.
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
private void doDecode(PrintStream export, int keysize, String fileKey, String fileOut,
String fileSrc, Object exargs) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] bytesB64Src = ZlRsaUtil.fileLoadBytes(fileSrc);
byte[] bytesSrc = Base64.decode(bytesB64Src);
if (null==bytesSrc || bytesSrc.length<=0) {
export.println(String.format("Error: %s is not BASE64!", fileSrc));
return;
}
String strDataKey = new String(ZlRsaUtil.fileLoadBytes(fileKey));
Map<String, String> map = new HashMap<String, String>();
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, map);
String purposecode = map.get(ZlRsaUtil.PURPOSE_CODE);
//out.println(bytesKey);
// key.
KeyFactory kf = KeyFactory.getInstance(ZlRsaUtil.RSA);
Key key= null;
//boolean isPrivate = false;
if ("R".equals(purposecode)) {
//isPrivate = true;
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
RSAPrivateKeySpec keySpec = (RSAPrivateKeySpec)kf.getKeySpec(key, RSAPrivateKeySpec.class);
keysize = keySpec.getModulus().bitLength();
} else { // 公鑰或無法判斷時, 均當成公鑰處理.
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
RSAPublicKeySpec keySpec = (RSAPublicKeySpec)kf.getKeySpec(key, RSAPublicKeySpec.class);
keysize = keySpec.getModulus().bitLength();
}
export.println(String.format("key.getAlgorithm: %s", key.getAlgorithm()));
export.println(String.format("key.getFormat: %s", key.getFormat()));
// decrypt.
Cipher cipher = Cipher.getInstance(ZlRsaUtil.RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] cipherBytes = ZlRsaUtil.decrypt(cipher, keysize, bytesSrc);
ZlRsaUtil.fileSaveBytes(fileOut, cipherBytes, 0, cipherBytes.length);
export.println(String.format("%s save done.", fileOut));
}
public static void main(String[] args) {
RsaPemDemo demo = new RsaPemDemo();
demo.run(System.out, args);
}
}
附錄.2 .Net版
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Collections;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
namespace RsaPemDemo {
/// <summary>
/// Java/.NET RSA demo, use pem key file (Java/.NET的RSA加解密演示項目,使用pem格式的密鑰文件).
/// </summary>
class Program {
/// <summary>
/// 幫助文本.
/// </summary>
private const string helpText = "Usage: RsaPemDemo [options] srcfile\n\nFor example:\n\n # encode by public key\n rsapemdemo -e -l publickey.pem -o dstfile srcfile\n\n # decode by private key\n rsapemdemo -d -l privatekey.pem -o dstfile srcfile\n\nThe options:\n\n -e RSA encryption and BASE64 encode.\n -d BASE64 decode and RSA decryption.\n -l [keyfile] Load key file.\n -o [outfile] out file.\n";
/// <summary>
/// 運行.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="args">參數.</param>
public void run(TextWriter export, string[] args) {
bool showhelp = true;
// args
string state = null; // 狀態.
bool isEncode = false;
bool isDecode = false;
string fileKey = null;
string fileOut = null;
string fileSrc = null;
int keysize = 0; // RSA密鑰位數. 0表示自動獲取.
foreach(string s in args) {
if ("-e".Equals(s, StringComparison.OrdinalIgnoreCase)) {
isEncode = true;
} else if ("-d".Equals(s, StringComparison.OrdinalIgnoreCase)) {
isDecode = true;
} else if ("-l".Equals(s, StringComparison.OrdinalIgnoreCase)) {
state = "l";
} else if ("-o".Equals(s, StringComparison.OrdinalIgnoreCase)) {
state = "o";
} else {
if ("l".Equals(state, StringComparison.OrdinalIgnoreCase)) {
fileKey = s;
state = null;
} else if ("o".Equals(state, StringComparison.OrdinalIgnoreCase)) {
fileOut = s;
state = null;
} else {
fileSrc = s;
}
}
}
try{
if (string.IsNullOrEmpty(fileKey)) {
export.WriteLine("No key file! Command need add `-l [keyfile]`.");
} else if (string.IsNullOrEmpty(fileOut)) {
export.WriteLine("No out file! Command need add `-o [outfile]`.");
} else if (string.IsNullOrEmpty(fileSrc)) {
export.WriteLine("No src file! Command need add `[srcfile]`.");
} else if (isEncode!=false && isDecode!=false) {
export.WriteLine("No set Encode/Encode! Command need add `-e`/`-d`.");
} else if (isEncode) {
showhelp = false;
doEncode(export, keysize, fileKey, fileOut, fileSrc, null);
} else if (isDecode) {
showhelp = false;
doDecode(export, keysize, fileKey, fileOut, fileSrc, null);
}
} catch (Exception ex) {
export.WriteLine(ex.ToString());
}
// do.
if (showhelp) {
export.WriteLine(helpText);
}
}
/// <summary>
/// 進行加密.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="keysize">密鑰位數. 為0表示自動獲取.</param>
/// <param name="fileKey">密鑰文件.</param>
/// <param name="fileOut">輸出文件.</param>
/// <param name="fileSrc">源文件.</param>
/// <param name="exargs">擴展參數.</param>
private void doEncode(TextWriter export, int keysize, string fileKey, string fileOut,
string fileSrc, IDictionary exargs) {
byte[] bytesSrc = File.ReadAllBytes(fileSrc);
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公鑰或無法判斷時, 均當成公鑰處理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
// encrypt.
byte[] cipherBytes = ZlRsaUtil.Encrypt(rsa, bytesSrc);
string cipherBase64 = Convert.ToBase64String(cipherBytes);
File.WriteAllText(fileOut, cipherBase64);
export.WriteLine(string.Format("{0} save done.", fileOut));
}
/// <summary>
/// 進行解密.
/// </summary>
/// <param name="export">文本打印流.</param>
/// <param name="keysize">密鑰位數. 為0表示自動獲取.</param>
/// <param name="fileKey">密鑰文件.</param>
/// <param name="fileOut">輸出文件.</param>
/// <param name="fileSrc">源文件.</param>
/// <param name="exargs">擴展參數.</param>
private void doDecode(TextWriter export, int keysize, string fileKey, string fileOut,
string fileSrc, IDictionary exargs) {
String bytesSrcB64Src = File.ReadAllText(fileSrc);
byte[] bytesSrc = Convert.FromBase64String(bytesSrcB64Src);
string strDataKey = File.ReadAllText(fileKey);
string purposetext = null;
char purposecode = '\0';
byte[] bytesKey = ZlRsaUtil.PemUnpack(strDataKey, ref purposetext, ref purposecode);
//export.WriteLine(bytesKey);
// key.
RSACryptoServiceProvider rsa;
if ('R' == purposecode) {
rsa = ZlRsaUtil.PemDecodePkcs8PrivateKey(bytesKey); // try
if (null == rsa) {
rsa = ZlRsaUtil.PemDecodeX509PrivateKey(bytesKey);
}
} else { // 公鑰或無法判斷時, 均當成公鑰處理.
rsa = ZlRsaUtil.PemDecodePublicKey(bytesKey);
}
if (null == rsa) {
export.WriteLine("Key decode fail!");
return;
}
export.WriteLine(string.Format("KeyExchangeAlgorithm: {0}", rsa.KeyExchangeAlgorithm));
export.WriteLine(string.Format("KeySize: {0}", rsa.KeySize));
// encryption.
byte[] cipherBytes = ZlRsaUtil.Decrypt(rsa, bytesSrc);
File.WriteAllBytes(fileOut, cipherBytes);
export.WriteLine(string.Format("{0} save done.", fileOut));
}
static void Main(string[] args) {
Program demo = new Program();
demo.run(Console.Out, args);
}
}
}
源碼地址:
https://github.com/zyl910/rsapemdemo
參考文獻
- 《RSA (cryptosystem)》: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
- Michel I. Gallant Ph.D.《RSA Public, Private, and PKCS #8 key parser》(OpenSSLKey.cs). http://www.jensign.com/opensslkey/
- 《PKCS#1:RSA加密》. http://man.chinaunix.net/develop/rfc/RFC2313.txt
- 《在線生成生成RSA密鑰對》. http://web.chacuo.net/netrsakeypair
- 蔣國綱《那些證書相關的玩意兒(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》. http://www.cnblogs.com/guogangj/p/4118605.html
- 阮一峰《RSA算法原理(二)》. http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
- 任家《OPENSSL中RSA私鑰文件(PEM格式)解析【一】》. http://blog.sina.com.cn/s/blog_4fcd1ea30100yh4s.html
- 寫代碼的二妹《PHP,C# 和JAVARSA簽名及驗簽》. http://www.cnblogs.com/frankyou/p/5993756.html
- FrankYou《C# RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993756.html
- FrankYou《Java RSA 分段加解密》. http://www.cnblogs.com/frankyou/p/5993685.html
- sahusoft《分組對稱加密模式:ECB/CBC/CFB/OFB缺CTR》. http://blog.csdn.net/sahusoft/article/details/6867848