朋友碰到调用第三方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-INFMAINFEST.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调用,读取输出流,解析即可。