RSA私钥加密研究

朋友碰到调用第三方API的加密问题,JAVA代码中用pfx私钥文件来加密字符串,流程如下: 

  • 输入私钥文件地址pfxPath、私钥密码pfxKey、被加密串dataContent
  • dataContent转成base64串,使用sun.misc.BASE64Decoder包
  • 用pfx私钥及PKCS12方式生成privateKey
  • privateKey和RSA/ECB/PKCS1Padding加密方式生成加密字节数组,再转成十六进制字符串

需求是在.net程序中得到同样的加密字符串,常见方法如下:

  1. 使用.net framework中相应的加密类实现同样的算法
  2. #1失败,根据原理,实现同样的算法
  3. 使用工具把java/jar包转成.net程序能调用的dll,如IKVM.NET,下载:http://www.ikvm.net/download.html 
  4. 将调用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调用,读取输出流,解析即可。

 

posted @ 2015-08-28 15:56  Tang.Lang  阅读(8110)  评论(0编辑  收藏  举报