Java RSA密钥文件处理

最近,在跟外部的一个服务进行接口对接,需要对调用双方的身份进行验证。采用了RSA非对称签名对方式来进行验证。主要记录一下Java读取OpenSSL生成的RSA密钥对的操作过程。

1.使用openssl生成private key

1
openssl genrsa -out private.pem 2048

生成一个新的长度为2048位RSA private key。密钥存储在private.pem文件中,密钥文件是PEM格式。PEM格式实际上就是对DER结构的Base64编码,查看该文件,可以看到文件以”BEGIN RSA PRIVATE KEY”为头,”END RSA PRIVATE KEY”为结尾。

1
2
3
4
5
head -2 private.pem; tail -1 private.pem

-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAwIEbtGxpXORlhW3SYaKzHzpKZgDOGGaJd7fD6ZJnaUesVWrr
-----END RSA PRIVATE KEY-----

2.生成public key

1
openssl rsa -in private.pem -out public.pem -pubout

生成的公钥也是PEM格式。

1
2
3
4
5
head -2 public.pem; tail -1 public.pem

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwIEbtGxpXORlhW3SYaKz
-----END PUBLIC KEY-----

3. 解析生成的Key

由于Java不能直接加载openssl生成的PEM格式,这里需要做一下转换。这里插一句关于PEM的说明,PEM实际上是DER编码然后进行Base64编码,再加上对Key进行说明的头和尾。

1
2
-inform DER|NET|PEM
This specifies the input format. The DER option uses an ASN1 DER encoded form compatible with the PKCS#1 RSAPrivateKey or SubjectPublicKeyInfo format.The PEM form is the default format: it consists of the DER format base64 encoded with additional header and footer lines. On input PKCS#8 format private keys are also accepted. The NET form is a format is described in the NOTES section.

在PKCS#1(RFC 3447)标准中定义了RSAPrivateKey和SubjectPublickeyInfo的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}

由于Java可以解析DER格式,所以转换的思路是去掉PEM文件中的头和尾,然后再Base64解码。

3.1 解析Public Key

代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 //此处省略部分代码
private static final String ALG = "RSA";

private static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----";

private static final String PUBLIC_KEY_FOOTER = "-----END PUBLIC KEY-----";

//此处省略部分代码

// 解析PublicKey
public static Optional<PublicKey> getDERPublicKeyFromPEMString(String keyString) {
String content = keyString
.replace(PUBLIC_KEY_HEADER, "")
.replace(PUBLIC_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

try {
KeyFactory keyFactory = KeyFactory.getInstance(ALG);
return Optional.of(keyFactory.generatePublic(new X509EncodedKeySpec(contentBytes)));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return Optional.empty();
}
}

代码中使用X509EncodedKeySpec来加载public key,X509EncodedKeySpec是Java对SubjectPublickeyInfo的实现. Note: 代码中用到了commons-codec工具包来进行Base64解码

3.2 解析Private Key

暂时还不能采用同样的方法直接去解析openssl生成的private key文件。因为Java中能直接识别的private key编码格式是PKCS#8.这里就需先对刚才生成的private key文件进行一次转换。

1
openssl pkcs8 -in private.pem -topk8 -nocrypt -out privatekey_key.pem

转换后文件内容如下:

1
2
3
4
5
head -2 private_key.pem; tail -1 private_key.pem

-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAgRu0bGlc5GWF
-----END PRIVATE KEY-----

注意,在这里没有对进行加密。然后再进行解析,代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

// 省略部分代码

private static final String UNENCRYPTED_PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----";

private static final String UNENCRYPTED_PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----";

private static final String ENCRYPTED_PRIVATE_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";

private static final String ENCRYPTED_PRIVATE_KEY_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";

// 省略部分代码

// 解析PrivateKey
public static Optional<PrivateKey> getDERPrivateKeyFromPEMString(String keyString, String password) {
try {
KeyFactory keyFactory = KeyFactory.getInstance(ALG);
if (isEncrypted(keyString)) {
return Optional.ofNullable(keyFactory.generatePrivate(getEncryptedPrivateKeySpec(keyString, password)));
}
return Optional.ofNullable(keyFactory.generatePrivate(getUnencryptedPrivateKeySpec(keyString)));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return Optional.empty();
}
}

// 未加密的PrivateKey
public static PKCS8EncodedKeySpec getUnencryptedPrivateKeySpec(String keyString) {
String content = keyString
.replace(UNENCRYPTED_PRIVATE_KEY_HEADER, "")
.replace(UNENCRYPTED_PRIVATE_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

return new PKCS8EncodedKeySpec(contentBytes);
}

// 加密的PrivateKey
public static PKCS8EncodedKeySpec getEncryptedPrivateKeySpec(String keyString, String password) {
String content = keyString
.replace(ENCRYPTED_PRIVATE_KEY_HEADER, "")
.replace(ENCRYPTED_PRIVATE_KEY_FOOTER, "")
.replaceAll("\\s", "");
byte[] contentBytes = Base64.decodeBase64(content);

try {
EncryptedPrivateKeyInfo epkInfo = new EncryptedPrivateKeyInfo(contentBytes);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
SecretKey secretKey = keyFactory.generateSecret(pbeKeySpec);

Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
cipher.init(Cipher.DECRYPT_MODE, secretKey, epkInfo.getAlgParameters());

return epkInfo.getKeySpec(cipher);

} catch (IOException | NoSuchAlgorithmException
| InvalidKeySpecException | NoSuchPaddingException
| InvalidKeyException | InvalidAlgorithmParameterException e) {
return null;
}
}

private static boolean isEncrypted(String keyString) {
return keyString.contains(ENCRYPTED_PRIVATE_KEY_HEADER);
}