全面解決.Net與Java互通時的RSA加解密問題,使用PEM格式的密鑰文件

做者: zyl910html

1、原因

RSA是一種經常使用的非對稱加密算法。因此有時須要在不用編程語言中分別使用RSA的加密、解密。例如用Java作後臺服務端,用C#開發桌面的客戶端軟件時。 因爲 .Net、Java 的RSA類庫存在不少細節區別,尤爲是它們支持的密鑰格式不一樣。致使容易出現「我加密的數據對方不能解密,對方加密的數據我不能解密,可是自身是能夠正常加密解密」等狀況。 雖然網上已經有不少文章討論 .Net與Java互通的RSA加解密,可是存在不夠全面、須要第三方dll、方案複雜 等問題。 因而我仔細研究了這一課題,獲得了一些穩定可靠的代碼。如今將研究成果分享給你們。java

2、密鑰

2.1 RSA密鑰文件格式介紹

要保證 .Net與Java 兩端均能正常的加解密,其中的重中之重就是確立一種密鑰文件格式,使 .Net與Java 兩端均能正確的加載密鑰。git

.Net與Java內置類庫對密鑰文件格式的支持狀況——github

  • .Net: 支持xml格式的密鑰文件。
  • Java: 沒有直接提供對密鑰文件的支持,僅提供了 PKCS#八、X.509 等編碼的密鑰數據的解析類。

2.1.1 技術細節——密鑰文件爲何這麼複雜

看到 PKCS#八、X.509,你們是否有些頭暈了? 其實RSA的密鑰文件不止這2種,還有許多種存儲格式。可參考 蔣國綱《那些證書相關的玩意兒(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)》。web

爲何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個條件——編程語言

  1. 文本格式。這樣用記事本打開密鑰文件,可以方便的複製粘貼,且能做爲程序中的字符串常量。使用靈活,方便測試等。
  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 用法——

  1. 選擇「生成密鑰位數」。直接使用默認的「2048位」就行,由於2048位是目前主流的密鑰位數,且.Net、Java均支持該長度。
  2. 選擇「密鑰格式」。直接使用默認的「PKCS#8」就行,由於咱們也是採用這種格式。
  3. 填寫「證書密碼」。通常不用填寫。
  4. 點擊「生成密鑰對(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#八、X.509 等編碼的密鑰數據的解析類。因而須要咱們本身來作PEM解包。

我觀察了網上的PEM解包的源碼,發現它們通常是用字符串數組存儲「-----BEGIN」的各類模式,而後根據該數組查找字符串來來定位數據的。但該辦法並不穩定,容易遇到問題——

  1. BEGIN後面的文本內容不規範。例若有寫成「-----BEGIN PUBLIC KEY」開頭的,有寫成「-----BEGIN RSA PUBLIC KEY」開頭的,還有其餘各類五花八門的模式。
  2. BEGIN(或END)先後的減號(-)長度不定。不一樣工具生成的PEM文件中,減號(-)長度是不一樣的。
  3. 有時中間會有多餘的空格等空白字符。

因而我寫了個狀態機算法來解析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沒有簡單的提供該屬性,而是須要一些步驟來獲得,且公鑰、私鑰得使用不一樣的類。

  1. 調用 KeyFactory.getKeySpec 方法,傳遞EncodedKeySpec(公鑰爲X509EncodedKeySpec,私鑰爲PKCS8EncodedKeySpec),獲取 KeySpec(公鑰爲RSAPublicKeySpec,私鑰爲RSAPrivateKeySpec)。
  2. 隨後調用 KeySpec對象的 getModulus 方法獲取 Modulus(即n)。
  3. 獲取 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、加解密

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、測試驗證

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

參考文獻

相關文章
相關標籤/搜索