1. 概述

公开密钥密码学中(又称非对称密码学),加密机制依赖于两个Key:公钥和私钥。公钥用作加密,私钥则用作解密。使用公钥加密后所得的密文,只能用相对应的私钥才能解密并得到原本的明文。

在本教程中,我们将学习如何从PEM文件中读取公钥和私钥。

首先,我们将研究一些公钥密码学的重要概念。然后,我们将学习如何使用纯Java读取PEM文件,以及如何使用第三方库-BouncyCastle实现。

2. 概念

在我们开始之前,让我们了解一些关键概念。

X.509是密码学里公钥证书的格式标准。 X.509证书里含有公钥、身份信息和签名信息。

DER 是存储数据(如X.509证书、PKCS8私钥等)最流行的编码格式。 这是一种二进制编码,因此生成的内容无法使用文本编辑器查看。

PKCS8 是存储私钥信息的格式标准。 私钥可以选择使用对称算法进行加密。

不仅可以使用此标准处理RSA私钥,还可以处理其他算法。 PKCS8私钥通常通过PEM编码格式交换。

PEM 是 DER 证书的一种基于Base64编码机制。 PEM 还可以编码其他类型的数据,如公钥/私钥和证书请求。

PEM文件还包含标题和页脚,用于描述编码数据的类型:

-----BEGIN PUBLIC KEY-----
...DER编码证书的Base64编码...
-----END PUBLIC KEY-----

3. 纯 Java 实现

3.1. 读取 PEM 文件数据

首先我们从 PEM 文件中读取数据,将其保存在String中:

String key = new String(Files.readAllBytes(file.toPath()), Charset.defaultCharset());

3.2. 从 PEM 中获取公钥

编写一个工具方法,从PEM编码的字符串中获取公钥:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjtGIk8SxD+OEiBpP2/T
JUAF0upwuKGMk6wH8Rwov88VvzJrVm2NCticTk5FUg+UG5r8JArrV4tJPRHQyvqK
wF4NiksuvOjv3HyIf4oaOhZjT8hDne1Bfv+cFqZJ61Gk0MjANh/T5q9vxER/7TdU
NHKpoRV+NVlKN5bEU/NQ5FQjVXicfswxh6Y6fl2PIFqT2CfjD+FkBPU1iT9qyJYH
A38IRvwNtcitFgCeZwdGPoxiPPh1WHY8VxpUVBv/2JsUtrB/rAIbGqZoxAIWvijJ
Pe9o1TY3VlOzk9ASZ1AeatvOir+iDVJ5OpKmLnzc46QgGPUsjIyo6Sje9dxpGtoG
QQIDAQAB
-----END PUBLIC KEY-----

传入 File 参数

public static RSAPublicKey readPublicKey(File file) throws Exception {
    String key = new String(Files.readAllBytes(file.toPath()), Charset.defaultCharset());

    String publicKeyPEM = key
      .replace("-----BEGIN PUBLIC KEY-----", "")
      .replaceAll(System.lineSeparator(), "")
      .replace("-----END PUBLIC KEY-----", "");

    byte[] encoded = Base64.decodeBase64(publicKeyPEM);

    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
    return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}

首先需要删除头部、尾部以及换行符。然后需要将Base64编码的字符串解码为对应的二进制格式。

然后,加载到 X509EncodedKeySpec

最后,我们可以使用 KeyFactory 类从规范中生成一个公钥对象。

3.3. 从 PEM 中获取私钥

知道如何读取公钥后,私钥也非常类似。

我们将使用PKCS8格式的PEM编码的私钥,结构如下。

-----BEGIN PRIVATE KEY-----
...Base64编码过的key...
-----END PRIVATE KEY-----

正如我们之前学习的那样,我们需要一个能够处理PKCS8密钥编码规范的类:PKCS8EncodedKeySpec。

public RSAPrivateKey readPrivateKey(File file) throws Exception {
    String key = new String(Files.readAllBytes(file.toPath()), Charset.defaultCharset());

    String privateKeyPEM = key
      .replace("-----BEGIN PRIVATE KEY-----", "")
      .replaceAll(System.lineSeparator(), "")
      .replace("-----END PRIVATE KEY-----", "");

    byte[] encoded = Base64.decodeBase64(privateKeyPEM);

    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
    return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
}

4. 使用第三方库 - BouncyCastle

4.1. 读取公钥

知道了如何使用纯JAVA代码实现,下面我们学习如何使用 BouncyCastle

首先得到公钥:

public RSAPublicKey readPublicKey(File file) throws Exception {
    KeyFactory factory = KeyFactory.getInstance("RSA");

    try (FileReader keyReader = new FileReader(file);
      PemReader pemReader = new PemReader(keyReader)) {

        PemObject pemObject = pemReader.readPemObject();
        byte[] content = pemObject.getContent();
        X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content);
        return (RSAPublicKey) factory.generatePublic(pubKeySpec);
    }
}

在使用BouncyCastle时,我们需要了解一些重要的类:

  • PemReader – 接受一个 Reader 作为参数并解析其内容. 它会自动删除头尾 以及 将底层Base64 编码的PEM 数据解码为二进制格式
  • PemObject – 存储由 PemReader 生成的结果

我们来看另一种方式,它将 Java 类(X509EncodedKeySpec, KeyFactory)包装到 BouncyCastle 的自己的类(JcaPEMKeyConverter)中

public RSAPublicKey readPublicKeySecondApproach(File file) throws IOException {
    try (FileReader keyReader = new FileReader(file)) {
        PEMParser pemParser = new PEMParser(keyReader);
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
        SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject());
        return (RSAPublicKey) converter.getPublicKey(publicKeyInfo);
    }
}

4.2. 读取私钥

和前面类似,方法一

public RSAPrivateKey readPrivateKey(File file) throws Exception {
    KeyFactory factory = KeyFactory.getInstance("RSA");

    try (FileReader keyReader = new FileReader(file);
      PemReader pemReader = new PemReader(keyReader)) {

        PemObject pemObject = pemReader.readPemObject();
        byte[] content = pemObject.getContent();
        PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
        return (RSAPrivateKey) factory.generatePrivate(privKeySpec);
    }
}

方法二

public RSAPrivateKey readPrivateKeySecondApproach(File file) throws IOException {
    try (FileReader keyReader = new FileReader(file)) {

        PEMParser pemParser = new PEMParser(keyReader);
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
        PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(pemParser.readObject());

        return (RSAPrivateKey) converter.getPrivateKey(privateKeyInfo);
    }
}

如我们所见,我们只是将 SubjectPublicKeyInfo 替换为 PrivateKeyInfo 并将 RSAPublicKey 替换为 RSAPrivateKey

4.3. BouncyCastle 的优势

There are a couple of advantages provided by the BouncyCastle library.

One advantage is that we don’t need to manually skip or remove the header and the footer. Another one is that we’re not responsible for the Base64 decoding either. Therefore, we can write less error-prone code with BouncyCastle.

Moreover, the BouncyCastle library supports the PKCS1 format as well. Despite the fact that PKCS1 is also a popular format used to store cryptographic keys (only RSA keys), Java doesn't support it on its own.

使用 BouncyCastle 的好处。

一个优点是我们不需要手动跳过或删除页眉和页脚。另一个优点是我们也不负责 Base64 解码。 因此,我们可以使用 BouncyCastle 编写不易出错的代码。

此外,BouncyCastle 库也支持 PKCS1 格式。 尽管 PKCS1 也是一种用于存储加密密钥(仅 RSA 密钥)的流行格式,但 Java 本身并不支持它。

5. 总结

本文介绍了如何从 PEM 文件中读取公钥和私钥,包括使用纯 Java 和 BouncyCastle 库的方法。我们还学习了一些公钥加密的关键概念。完整源代码可以在 GitHub 上找到, Java源码BouncyCastle源码