C#与Java的RSA

前段时间的一个项目,涉及到在C#的项目中调用外部Java系统的接口,其中的登录功能要求C#端先与Java端协商RSA公钥,然后用公钥加密密码提交给Java端进行验证。Java端使用的是2048位的标准RSA加密,给出的公钥是一个HEX字符串,如:
30820122300D06092A864886F70D01010105000382010F003082010A02820101008C214751E6EA33378080F64BF55C0888D3EFA4DF08794318069DDFD14A3AB6468B20CD134819
100FA20539785AECF595CF2333F7ADC48366F4ACBC41B1CED728B57417CF3B6CA4E7DDB9DA348F7D38158DD6F2FF3934AEB0A70732E2949505EF893A940404B1B5F4B69243E287
7BBA90E5994EBFD61986F412DE4AD3E8331CE1D3D41ADAEF5C79D5B22E05C7F76FC748BC5FA42345D70EC3D1DE3DBD338C300C3750841E2E16E7B907E536FCA1A40D05DC9DFCDE
4EB2E8575228309AD146486E6F21C386E90C36DEECB57F955CE68609204AFBD434F8A1BFB5D921C470EED82CCA8BFDA92A8EC668E9E9EB6F959CD535C8BCCFCB08A671983A27E8
B03F5BF90D0203010001

RSA本身的算法不复杂,相信不少人都在项目中做过加解密,不同语言都封装得挺好,可是一但跨语言,问题就来了。正好借着这个项目,深入研究了一下。


首先看看这个公钥是怎么来的。在Java中,一般这样产生密钥对并且编码输出公钥:

/*********** Java代码 ***********/
         
//初始化2048位的RSA密钥生成器
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
         
//生成密钥对,并得到公钥与私钥
KeyPair keys = generator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey)keys.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey)keys.getPrivate();
         
//编码,并转化成HEX形式的字符串
String publicKeyHex = bytes2hex(publicKey.getEncoded());

System.out.println(publicKeyHex);

本文一开头那个3082开头的大字符串就是这么产生的。需要利用公钥加密时,在Java里一般这么做:


/*********** Java代码 ***********/
         
//还原公钥
KeyFactory factory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec spec = new X509EncodedKeySpec(hex2byte(publicKeyHex));
PublicKey publicKey = factory.generatePublic(spec);
         
//RSA公钥加密(无填充模式)
Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal("PASSWORD".getBytes("UTF-8"));
             
//密文转化成HEX字符串
String resultHex = byte2hex(result);

上面2段代码涉及到byte[]和HEX字符串的相互转化,顺便也放一下:

/*********** Java代码 ***********/
     
private byte[] hex2bytes(String hex) {
    String digital = "0123456789ABCDEF";
    char[] hex2char = hex.toCharArray();
    byte[] bytes = new byte[hex.length() / 2];
    int temp;
    for (int i = 0; i < bytes.length; i++) {
        temp = digital.indexOf(hex2char[2 * i]) * 16;
        temp += digital.indexOf(hex2char[2 * i + 1]);
        bytes[i] = (byte) (temp & 0xff);
    }
               
    return bytes;
}
         
private String bytes2hex(byte[] b) {
    String hs = "";
    String stmp = "";
    for (int n = 0; n < b.length; n++) {
        stmp = (java.lang.Integer.toHexString(b[n] & 0xFF));
        if (stmp.length() == 1)
            hs = hs + "0" + stmp;
        else
            hs = hs + stmp;
    }
         
    return hs.toUpperCase();
}

我相信绝大多数Java程序员从来没想过getEncoded()是什么算法,它是如何把modulus与publicExponent封装到一个byte[]里去的。而对于C#程序员来讲,RSA公钥加密必须要用到modulus与publicExponent,无论它们是XML形式还是byte[]形式,因此如何从publicKeyHex中解析这两个参数就成了第一个关键点。

我们看看JavaDoc上是怎么说的。

An Encoded Form

This is an external encoded form for the key used when a standard representation of the key is needed outside
the Java Virtual Machine, as when transmitting the key to some other party. The key is encoded according to
a standard format (such as X.509 SubjectPublicKeyInfo or PKCS#8), and is returned using the getEncoded method.
Note: The syntax of the ASN.1 type SubjectPublicKeyInfo is defined as follows:

SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }

AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }

For more information, see RFC 3280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile.

根据ASN.1标准进行保存的,涉及到了2种格式:X.509 SubjectPublicKeyInfo和PKCS#8, 具体是哪种可以从getFormat()的方法说明里可以找到答案:公钥使用的是X.509 SubjectPublicKeyInfo,私钥使用的是PKCS#8。


我们只关心公钥,OK,下一步来看看如何从中解出我们需要的modulus和publicExponent。很遗憾的说一句,.NetFramework中没有现成的类来处理ASN.1和SubjectPublicKeyInfo,想获取数据有这么几个方法:

1、利用Win32API,引入非托管代码crypt32.dll,调用CryptDecodeObject()方法来解码

2、利用第三方的类库,比如BouncyCastle,它有C#的实现,http://www.bouncycastle.org/csharp

3、自己分析


个人比较推荐BouncyCastle,不过本文打算“深入”研究一下,所以准备走第3条路。


在用Java生成publicKey的时候,把modulus和publicExponent也顺便输出一下用于检验,代码如下:

/*********** Java代码 ***********/
System.out.println(bytes2hex(publicKey.getModulus().toByteArray()));
System.out.println(bytes2hex(publicKey.getPublicExponent().toByteArray()));

C#的RSA加密不是标准RSA,为了加强安全性,它强制使用了PKCS1填充(或OAEP填充),导致密文在Java端解密失败,因为Java端用的是无填充模式(RSA/ECB/NoPadding)。


怎么办? 自己实现标准RSA加密吧。modulus和publicExponent都有了,再加上密码,都转化成大整数,做一个ModPow操作,就得到结果密文了。正好.NetFramework从4.0起在System.Numerics命名空间下直接提供BigInterger类了,上手试试:

/*********** C#代码 ***********/
                                             
//把3个变量转化为System.Numerics.BigInteger
var modulus = new BigInteger(modulusBytes.Reverse().ToArray());
var exponent = new BigInteger(exponentBytes.Reverse().ToArray());
var data = new BigInteger(Encoding.UTF8.GetBytes("PASSWORD").Reverse().ToArray());
                                             
//做ModPow运算得到密文,也是BigInteger
var result = BigInteger.ModPow(d, e, m);
                                             
//把密文BigInteger对应的byte[]转化为HEX形式
string resultHex = BitConverter.ToString(result.ToByteArray().Reverse().ToArray()).Replace("-", "");

这下终于OK了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值