.Net、Java 的RSA类库存在很多细节区别,尤其是它们支持的密钥格式不同

容易出现“我加密的数据对方不能解密,对方加密的数据我不能解密,但是自身是可以正常加密解密”等情况

.Net与Java内置类库对密钥文件格式的支持情况

  • .Net: 支持xml格式的密钥文件
  • Java: 没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类

密钥文件为什么这么复杂

RSA的密钥文件不止PKCS#8、X.509 2种,还有许多种存储格式

为什么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 等用于简化、校验加密的值

私钥文件的字节数,一般比公钥文件大一些


  • 公钥:X.509 pem

Java类为 X509EncodedKeySpec

  • 私钥:PKCS#8 pem

Java类为 PKCS8EncodedKeySpec

生成密钥

可用代码生成密钥对,.Net、Java类库有完善的支持

可用 OpenSSL 命令生成

在线工具生成

http://web.chacuo.net/netrsakeypair

Java加载密钥

PEM解包

PEM文件是以 -----BEGIN”开头、-----END 结尾,实际密钥数据BASE64给放在中间

Java没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。需要自己来做PEM解包

网上很多PEM解包的源码,一般用字符串数组存储“—–BEGIN”的各种模式,然后根据该数组查找字符串来来定位数据,容易遇到问题

  • BEGIN后面的文本内容不规范

有写成 —–BEGIN PUBLIC KEY”开头的,有写成 —–BEGIN RSA PUBLIC KEY”开头的,还有其他各种五花八门的模式

  • BEGIN(或END)前后的减号(-)长度不定
  • 不同工具生成的PEM文件中,减号(-)长度是不同
  • 有时中间会有多余的空格等空白字符

加载公钥

Java提供了X509EncodedKeySpec,加载公钥比较简单

byte[] bytesKey = xxxx;
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);

加载私钥

Java提供了PKCS8EncodedKeySpec,加载私钥比较简单

  byte[] bytesKey = xx;
  KeyFactory kf = KeyFactory.getInstance("RSA");
  Key key= null;
  PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
  key = kf.generatePrivate(spec);

判断密钥位数

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();

.net

仅提供对Xml密钥文件的支持,要自己写PEM的解包代码

没有提供 X.509 的解码类,需自己写

没有提供 PKCS#8 的解码类,也需要自己编写 PKCS#8的私钥数据,其实还嵌套了一层X.509编码,.net 得按顺序分别进行解码

判断密钥位数

RSACryptoServiceProvider.KeySize 便可得到密钥位数,非常简单

加解密

确立加密模式与填充方式

加密模式一般 ECB/CBC/CFB/OFB 四种

对RSA来说,ECB最简单但安全性比较弱,CBC等模式复杂且还需考虑IV(initialization vector,初始化向量)的管理

ECB是.Net的默认模式

由于加密算法都是按块来处理的,故理论上只有当明文长度正好是块长度的倍数时才能进行加解密

但这样太麻烦,有了填充方式的概念,在明文后面填充一些数据,使其长度正好是块的倍数

填充方式还有2个作用,一是能标记原始数据长度,使解码时自动去掉末尾的填充数据,二是能提高安全性

.Net RSA算法默认用PKCS#1填充,故Java中可选择 PKCS1Padding 填充方式

    /**
     * RSA .
     */
    public final static String RSA = "RSA";
    
    /**
     * 具体的 RSA 算法.
     */
    public final static String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";

分段加密

.Net、Java自带的RSA库来说,填充方式只是解决了“明文长度小于块尺寸”的问题

而当明文长度大于块尺寸时,便会抛出异常,常见的异常信息有

// .Net
不正确的长度

// Java
javax.crypto.IllegalBlockSizeException: Data must not be longer than xx bytes

此时便需要对数据进行分段加密

块尺寸的计算

密文的块尺寸是很容易计算的,即“密钥位数/8”。即把二进制长度转为字节长度

而明文的块尺寸的计算就稍微麻烦了一点,与填充方式有关

PKCS#1填充方式,需占用11个字节。于是块尺寸为“密钥位数/8 - 11”

例如密钥长度为2048位时

  • 密文的块尺寸 = 密钥位数/8 = 20488 = 256
  • 明文的块尺寸 = 密钥位数/8 - 11 = 20488 - 11 = 256 - 11 = 245

  • 加密时:明文的块为245字节,加密后输出的密文块为256字节
  • 解密时:密文的块为256字节,解密后输出的明文块为245字节

测试验证

  • Java 端加密生成密文文件,随后 Java 端读取密文文件做解密
  • .Net 端加密生成密文文件,随后 .Net 端读取密文文件做解密
  • Java 端加密生成密文文件,随后 .Net 端读取密文文件做解密
  • .Net 端加密生成密文文件,随后 Java 端读取密文文件做解密

4种测试都通过后,便表示加解密没问题

openssl

openssl genrsa -out rsa_private_key.pem 1024  

私钥内容

-----BEGIN RSA PRIVATE KEY-----  
xxx
-----END RSA PRIVATE KEY-----  

根据私钥生成公钥

openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout  
writing RSA key

公钥的内容

-----BEGIN PUBLIC KEY-----  
xxx
-----END PUBLIC KEY-----  

这时候的私钥还不能直接被java使用,需要进行PKCS#8编码

openssl pkcs8 -topk8 -in rsa_private_key.pem -out pkcs8_rsa_private_key.pem -nocrypt

输入私钥rsa_private_key.pem,输出私钥pkcs8_rsa_private_key.pem,不采用任何二次加密(-nocrypt)

 cat pkcs8_rsa_private_key.pem   
-----BEGIN PRIVATE KEY-----  
xxx
-----END PRIVATE KEY-----  

区别在

pkcs8的
-----BEGIN PRIVATE KEY----- 

第一步生成的私钥文件编码是PKCS#1格式
-----BEGIN RSA PRIVATE KEY-----  
    public void loadPrivateKey(String privateKeyStr) throws Exception {

        try {
            BASE64Decoder       base64Decoder = new BASE64Decoder();
            byte[]              buffer        = base64Decoder.decodeBuffer(privateKeyStr);
            PKCS8EncodedKeySpec keySpec       = new PKCS8EncodedKeySpec(buffer);
            KeyFactory          keyFactory    = KeyFactory.getInstance("RSA");
            this.privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("私钥非法");
        } catch (IOException e) {
            throw new Exception("私钥数据内容读取错误");
        } catch (NullPointerException e) {
            throw new Exception("私钥数据为空");
        }
    }

直接用第一步生成的话,加两行

public void loadPrivateKey(String privateKeyStr) throws Exception {

        try {
            BASE64Decoder       base64Decoder = new BASE64Decoder();
            byte[]              buffer        = base64Decoder.decodeBuffer(privateKeyStr);
 
            org.bouncycastle.asn1.pkcs.RSAPrivateKey  asn1PrivKey    = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(ASN1Sequence.fromByteArray(buffer));
            RSAPrivateKeySpec      rsaPrivKeySpec = new RSAPrivateKeySpec(asn1PrivKey.getModulus(), asn1PrivKey.getPrivateExponent());
            KeyFactory          keyFactory    = KeyFactory.getInstance("RSA");
            this.privateKey = (RSAPrivateKey) keyFactory.generatePrivate(rsaPrivKeySpec);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("无此算法");
        } catch (InvalidKeySpecException e) {
            throw new Exception("私钥非法");
        } catch (IOException e) {
            throw new Exception("私钥数据内容读取错误");
        } catch (NullPointerException e) {
            throw new Exception("私钥数据为空");
        }
    }