朋友碰到調用第三方API的加密問題,JAVA代碼中用pfx私鑰文件來加密字符串,流程如下:
- 輸入私鑰文件地址pfxPath、私鑰密碼pfxKey、被加密串dataContent
- dataContent轉成base64串,使用sun.misc.BASE64Decoder包
- 用pfx私鑰及PKCS12方式生成privateKey
- privateKey和RSA/ECB/PKCS1Padding加密方式生成加密字節數組,再轉成十六進制字符串
需求是在.net程序中得到同樣的加密字符串,常見方法如下:
- 使用.net framework中相應的加密類實現同樣的算法
- #1失敗,根據原理,實現同樣的算法
- 使用工具把java/jar包轉成.net程序能調用的dll,如IKVM.NET,下載:http://www.ikvm.net/download.html
- 將調用java生成加密串的代碼打成jar包,包含在命令行中,在.net程序中調用,取得結果
照理說,第4種方法是最簡單快速的,不過屬於暴力破解的法子,按常規的思路,我還是第1種方法入手。
首先我很奇怪為什么有API是用私鑰來加密,雖然說公鑰私鑰交換使用是可以的,但什么場景會這樣使用?知乎有些推論:http://www.zhihu.com/question/25912483
但是如果你想發布一個公告,需要一個手段來證明這確實是你本人發的,而不是其他人冒名頂替的。那你可以在你的公告開頭或者結尾附上一段用你的私鑰加密的內容(例如說就是你公告正文的一段話),那所有其他人都可以用你的公鑰來解密,看看解出來的內容是不是相符的。如果是的話,那就說明這公告確實是你發的---因為只有你的公鑰才能解開你的私鑰加密的內容,而其他人是拿不到你的私鑰的。
從現象來說,公鑰加密,每次得到的加密信息都不固定,私鑰加密得到的加密信息是固定的。可能基於這些原因,此API才用私鑰來加密吧。
C#提供的RSA算法類有RSACryptoServiceProvider,它的實現按常規的做法,公鑰加密,私鑰解密,默認情況下,沒有提供用私鑰加密的現成方法,#1方法失效;
網上有用私鑰加密的實現,類似的參考有:
C#使用RSA進行私鑰加密公鑰解密(蝸牛大俠), http://blog.csdn.net/a351945755/article/details/21965533
基於私鑰加密公鑰解密的RSA算法C#實現(zhilunchen),http://blog.csdn.net/zhilunchen/article/details/2943158,
C#使用RSA私鑰加密公鑰解密的改進,解決特定情況下解密后出現亂碼的問題,http://www.byywee.com/page/M0/S545/545934.html
BigInteger類下載:http://www.codeproject.com/Articles/2728/C-BigInteger-Class
但朋友和我驗證后都失敗了,得出來的加密串與java得出的不一致,關鍵的算法如下:
//paramsters是C#加載私鑰文件后輸出的RSAParameters對象 BigInteger d = new BigInteger(paramsters.D); BigInteger n = new BigInteger(paramsters.Modulus); BigInteger biText = new BigInteger(context); //context是被加密串轉成base64后取字節數組 BigInteger biEnText = biText.modPow(d, n);
看上去可能是算法不一樣所致,有必要去查看一下java是如何實現的。
Java加密串部分:
Cipher cipher = Cipher.getInstance(RsaConst.RSA_CHIPER);// RSA_CHIPER = "RSA/ECB/PKCS1Padding"; cipher.init(mode, privateKey); //mode = Cipher.ENCRYPT_MODE = 1 byte[] doFinal = cipher.doFinal(subarray(srcData, i, i + blockSize));
Cipher是個基類,從“RSA/ECB/PKCS1Padding”找到com.sun.crypto.provider.RSACipher(源碼地址)
再找到它執行的方法:doFinal()(源碼地址)
這兩句就是真正的執行代碼:
data = padding.pad(buffer, 0, bufOfs); return RSACore.rsa(data, privateKey);
接下來找到sun.security.rsa.RSACore (源碼地址)
public static byte[] rsa(byte[] msg, RSAPrivateKey key) throws BadPaddingException { if (key instanceof RSAPrivateCrtKey) { return crtCrypt(msg, (RSAPrivateCrtKey)key); } else { return priCrypt(msg, key.getModulus(), key.getPrivateExponent()); } }
這里有兩個方法,crtCrypt和priCrypt,那么到底執行哪種方法呢?取決於加載的私鑰文件是哪種類型,這點很容易驗證,調試java代碼就可以獲知,朋友提供的私鑰文件是實現了sun.security.rsa.RSAPrivateCrtKeyImpl的RSAPrivateCrtKey類,所以它會執行crtCrypt方法。
private static byte[] crtCrypt(byte[] msg, RSAPrivateCrtKey key) throws BadPaddingException { BigInteger n = key.getModulus(); BigInteger c = parseMsg(msg, n); BigInteger p = key.getPrimeP(); BigInteger q = key.getPrimeQ(); BigInteger dP = key.getPrimeExponentP(); BigInteger dQ = key.getPrimeExponentQ(); BigInteger qInv = key.getCrtCoefficient(); BigInteger e = key.getPublicExponent(); BigInteger d = key.getPrivateExponent(); BlindingRandomPair brp; if (ENABLE_BLINDING) { brp = getBlindingRandomPair(e, d, n); c = c.multiply(brp.u).mod(n); } // m1 = c ^ dP mod p BigInteger m1 = c.modPow(dP, p);
// m2 = c ^ dQ mod q BigInteger m2 = c.modPow(dQ, q);
// h = (m1 - m2) * qInv mod p BigInteger mtmp = m1.subtract(m2);
if (mtmp.signum() < 0) { mtmp = mtmp.add(p); }
BigInteger h = mtmp.multiply(qInv).mod(p); // m = m2 + q * h BigInteger m = h.multiply(q).add(m2); if (ENABLE_BLINDING) { m = m.multiply(brp.v).mod(n); } return toByteArray(m, getByteLength(n)); }
用C#來實現同樣的算法費時頗多,先用#3的方法來試驗一下,用工具IKVM把相關的jar包生成dll,在C#調用調試。
Java中privateKey的屬性與C#中RSAParameters中的屬性對比:(原理在這)
d=Q; e=Exponent;n=Modulus;p=P;pe=DP;q=Q;qe=DQ;encodedKey在C#中的byte[]數組與java的byte[]轉成的C#的sbyte[]數組相等;
在C#中把一個個方法拆下來運行,結果與java生成的都不一致,與這些算法相關的類較多,如BlindingRandomPair,RSAPadding等,一個個實現很費時,研究下相關的源碼也是一樂趣。
同時發現網上所寫的C#代碼用私鑰加密的算法,與java中用公鑰加密的算法一樣,但是不能替代java中的私鑰加密算法。請看對比:
public static byte[] rsa(byte[] msg, RSAPublicKey key) throws BadPaddingException { return crypt(msg, key.getModulus(), key.getPublicExponent()); } private static byte[] crypt(byte[] msg, BigInteger n, BigInteger exp) throws BadPaddingException { BigInteger m = parseMsg(msg, n); BigInteger c = m.modPow(exp, n); return toByteArray(c, getByteLength(n)); }
在與上面所寫的C#自寫的私鑰加密關鍵部分:
//paramsters是C#加載私鑰文件后輸出的RSAParameters對象 BigInteger d = new BigInteger(paramsters.D); BigInteger n = new BigInteger(paramsters.Modulus); BigInteger biText = new BigInteger(context); //context是被加密串轉成base64后取字節數組 BigInteger biEnText = biText.modPow(d, n);
除了取publicExponent和D算子不同外(因為key類型不一樣)。
綜上所述,第3種方法和第4種方法都是可以解決的,但實質還是在java環境下運行。
第3種方法的概略是:
- 封裝好調用,Export成jar包,修改jar包中的META-INF\MAINFEST.MF文件,設置Main-Class和Class-Path(可能會包含其它的jar包,如果沒有則不設置)
- 可以用ikvm –jar xxx.jar驗證一下,看是否能正常運行Main中的測試代碼(如果包含其它jar包,最好放在同一路徑下)
- 用ikvmc –target:library xxx.jar (lib1.jar lib2.jar)命令生成相應的dll文件
- 在C#項目中引用,測試(必須引入IKVM.OpenJDK.Core)
第4種方法則很簡單,用java命令調用,或者用bat封裝命令,在代碼中用Process調用,讀取輸出流,解析即可。