rsa
dev
#crypto
.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
然后公钥与私钥分别要存储不同的值
对常用的X.509等编码的私钥文件中,不仅存储了 n、e、d、p、q,还存储了 d mod (p-1)、d mod (q-1)、(inverse of q) mod p 等用于简化、校验加密的值
私钥文件的字节数,一般比公钥文件大一些
生成密钥
可用代码生成密钥对,.Net、Java类库有完善的支持
可用 OpenSSL 命令生成
在线工具生成
http://web.chacuo.net/netrsakeypair
Java加载密钥
PEM解包
PEM文件是以 -----BEGIN”开头、-----END 结尾
,实际密钥数据BASE64给放在中间
Java没有直接提供对密钥文件的支持,仅提供了 PKCS#8、X.509 等编码的密钥数据的解析类。需要自己来做PEM解包
网上很多PEM解包的源码,一般用字符串数组存储“—–BEGIN”的各种模式,然后根据该数组查找字符串来来定位数据,容易遇到问题
有写成 —–BEGIN PUBLIC KEY”开头的,有写成 —–BEGIN RSA PUBLIC KEY”开头的,还有其他各种五花八门的模式
- BEGIN(或END)前后的减号(-)长度不定
- 不同工具生成的PEM文件中,减号(-)长度是不同
- 有时中间会有多余的空格等空白字符
加载公钥
Java提供了X509EncodedKeySpec,加载公钥比较简单
1
2
3
4
5
|
byte[] bytesKey = xxxx;
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytesKey);
key = kf.generatePublic(spec);
|
加载私钥
Java提供了PKCS8EncodedKeySpec,加载私钥比较简单
1
2
3
4
5
|
byte[] bytesKey = xx;
KeyFactory kf = KeyFactory.getInstance("RSA");
Key key= null;
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesKey);
key = kf.generatePrivate(spec);
|
判断密钥位数
Java没有简单的提供该属性,而是需要一些步骤来得到,且公钥、私钥得使用不同的类
- 调用 KeyFactory.getKeySpec 方法,传递EncodedKeySpec(公钥为X509EncodedKeySpec,私钥为PKCS8EncodedKeySpec),获取 KeySpec(公钥为RSAPublicKeySpec,私钥为RSAPrivateKeySpec)
- 调用 KeySpec对象的 getModulus 方法获取 Modulus(即n)
- 获取 Modulus(即n)的位数,它就是密钥位数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 填充方式
1
2
3
4
5
6
7
8
9
|
/**
* RSA .
*/
public final static String RSA = "RSA";
/**
* 具体的 RSA 算法.
*/
public final static String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
分段加密
.Net、Java自带的RSA库来说,填充方式只是解决了“明文长度小于块尺寸”的问题
而当明文长度大于块尺寸时,便会抛出异常,常见的异常信息有
1
2
3
4
5
|
// .Net
不正确的长度
// Java
javax.crypto.IllegalBlockSizeException: Data must not be longer than xx bytes
|
此时便需要对数据进行分段加密
块尺寸的计算
密文的块尺寸是很容易计算的,即“密钥位数/8”。即把二进制长度转为字节长度
而明文的块尺寸的计算就稍微麻烦了一点,与填充方式有关
PKCS#1填充方式,需占用11个字节。于是块尺寸为“密钥位数/8 - 11”
例如密钥长度为2048位时
- 密文的块尺寸 = 密钥位数/8 = 2048/8 = 256
- 明文的块尺寸 = 密钥位数/8 - 11 = 2048/8 - 11 = 256 - 11 = 245
即
- 加密时:明文的块为245字节,加密后输出的密文块为256字节
- 解密时:密文的块为256字节,解密后输出的明文块为245字节
测试验证
- Java 端加密生成密文文件,随后 Java 端读取密文文件做解密
- .Net 端加密生成密文文件,随后 .Net 端读取密文文件做解密
- Java 端加密生成密文文件,随后 .Net 端读取密文文件做解密
- .Net 端加密生成密文文件,随后 Java 端读取密文文件做解密
4种测试都通过后,便表示加解密没问题
openssl
1
|
openssl genrsa -out rsa_private_key.pem 1024
|
私钥内容
1
2
3
|
-----BEGIN RSA PRIVATE KEY-----
xxx
-----END RSA PRIVATE KEY-----
|
根据私钥生成公钥
1
2
|
openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout
writing RSA key
|
公钥的内容
1
2
3
|
-----BEGIN PUBLIC KEY-----
xxx
-----END PUBLIC KEY-----
|
这时候的私钥还不能直接被java使用,需要进行PKCS#8编码
1
|
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)
1
2
3
4
|
cat pkcs8_rsa_private_key.pem
-----BEGIN PRIVATE KEY-----
xxx
-----END PRIVATE KEY-----
|
区别在
1
2
3
4
5
|
pkcs8的
-----BEGIN PRIVATE KEY-----
第一步生成的私钥文件编码是PKCS#1格式
-----BEGIN RSA PRIVATE KEY-----
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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("私钥数据为空");
}
}
|
直接用第一步生成的话,加两行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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("私钥数据为空");
}
}
|