使用openssl生成一个新的密钥对

您首先使用 genrsa OpenSSL中的工具生成私钥:

openssl genrsa -out privatekey.pem 2048

这将创建一个具有2048位长度的新的RSA私钥。密钥存储在文件中 privatekey.pem,它是“PEM”格式。PEM格式本质上是DER编码结构的base64编码变体。您可以查看文件,它应该以“BEGIN RSA PRIVATE KEY”标题开头,并以“END RSA PRIVATE KEY”页尾结尾:

head -2 privatekey.pem; tail -1 privatekey.pem 
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAth6P/MXUGL1y69Ao9THV16taSeUWnM4FQpmHP0yMDS3hB4V0
-----END RSA PRIVATE KEY-----

现在我们需要开始这个文件。虽然这似乎只是私钥,公钥似乎丢失了,但并不是:这个私钥格式包含了重建公钥数据的所有信息。

提取公钥

我们将使用的OpenSSL的第二个工具是rsa。这允许对密钥文件进行一些转换。

openssl rsa -in privatekey.pem -out publickey.pem -pubout

如果我们看生成文件 publickey.pem,我们看到,也是PEM格式。页眉和页脚行分别是“BEGIN PUBLIC KEY”和“END PUBLIC KEY”:

head -2 publickey.pem; tail -1 publickey.pem 
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAth6P/MXUGL1y69Ao9THV
-----END PUBLIC KEY-----

现在我们有明文密钥文件可用。您可以分发公钥文件,以允许对方加密一些数据,同时保留私钥。请注意,私钥文件未加密,必须以某种方式保护(如文件权限等)。

了解关键文件结构

Java本身不能直接加载在上述步骤中生成的PEM文件。然而,PEM文件实际上只是用附加的页眉和页脚编码的“DER”格式基础64。但是什么是“DER”格式?该手册页解释了一点:

-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定义了结构RSAPrivateKey和SubjectPublicKeyInfo。该标准也作为RFC 3447出版。附录A.1.2中描述了交换私钥的建议:

 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
 }

你可以看到两件事情:结构基本上是数字列表。私钥结构包含模数,这就是您可以从此私钥文件中提取公钥的原因。

公钥结构 SubjectPublicKeyInfo附录A.1.1所述

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

您可以使用OpenSSL以“人类可读”格式显示此信息:

openssl rsa -in privatekey.pem -text
Private-Key: (2048 bit)
modulus:
    00:b6:1e:8f:fc:c5:d4:18:bd:72:eb:d0:28:f5:31:
...
publicExponent: 65537 (0x10001)
privateExponent:
    00:a9:f4:cb:9a:b1:63:c5:d2:c6:b4:9a:86:1e:8c:
... It will display all the fields. The same is possible with the public key:

openssl rsa -in publickey.pem -text -pubin
Public-Key: (2048 bit)
Modulus:
    00:b6:1e:8f:fc:c5:d4:18:bd:72:eb:d0:28:f5:31:
...

将密钥文件转换为Java(公共密钥)

普通Java能够理解公钥格式。但是,它不能直接读取PEM文件,但可以理解DER编码。解决方案是,首先使用Base64对文件进行解码,然后通过Java进行解析。这是一个这样做的代码段:

public static PublicKey loadPublicKey() throws Exception {
    String publicKeyPEM = FileUtils.readFileToString(new File("publickey.pem"), StandardCharsets.UTF_8);

    // strip of header, footer, newlines, whitespaces
    publicKeyPEM = publicKeyPEM
            .replace("-----BEGIN PUBLIC KEY-----", "")
            .replace("-----END PUBLIC KEY-----", "")
            .replaceAll("\\s", "");

    // decode to get the binary DER representation
    byte[] publicKeyDER = Base64.getDecoder().decode(publicKeyPEM);

    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyDER));
    return publicKey;
}

它用于 X509EncodedKeySpec 加载公钥,这只是推荐的 SubjectPublicKeyInfo 实现。

注意:我使用 Apache的commons-ioFileUtils。其他一切都包含在标准的Java8 JDK中。

转换私钥用于Java

不幸的是,我们不能使用完全相同的技巧来进行私钥。Java具有私钥的编码密钥规范:PKCS8EncodedKeySpec - 但是,它实现了“PKCS#8”而不是我们使用的“PKCS#1”。幸运的是,OpenSSL还包含一个这种格式的转换器:

openssl pkcs8 -in privatekey.pem -topk8 -nocrypt -out privatekey-pkcs8.pem

如果您检查生成的文件,您将再次看到PEM格式,但现在标题为“BEGIN PRIVATE KEY”:

head -2 privatekey-pkcs8.pem; tail -1 privatekey-pkcs8.pem
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2Ho/8xdQYvXLr
-----END PRIVATE KEY-----

请注意,这个私钥文件也没有加密(nocrypt),并且必须保持安全。
该格式在 RFC 5208第5节中 的结构中 描述:

 PrivateKeyInfo ::= SEQUENCE {
   version                   Version,
   privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
   privateKey                PrivateKey,
   attributes           [0]  IMPLICIT Attributes OPTIONAL }

它再次被DER编码,它实际上只是 RSAPrivateKeyprivateKey 字段中从上面包装结构。但是,此格式允许使用密码加密私钥(我们在此示例中不使用)。

现在我们也可以在Java中加载私钥:

public static PrivateKey loadPrivateKey() throws Exception {
    String privateKeyPEM = FileUtils.readFileToString(new File("privatekey-pkcs8.pem"), StandardCharsets.UTF_8);

    // strip of header, footer, newlines, whitespaces
    privateKeyPEM = privateKeyPEM
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s", "");

    // decode to get the binary DER representation
    byte[] privateKeyDER = Base64.getDecoder().decode(privateKeyPEM);

    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyDER));
    return privateKey;
}

使用RSA在Java中加密和解密

现在我们可以使用Java加密和解密,如下所示:

public static void main(String[] args) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    String clearText = "Sample plain text";

    PublicKey publicKey = loadPublicKey();
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    byte[] encrypted = cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));

    PrivateKey privateKey = loadPrivateKey();
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] decrypted = cipher.doFinal(encrypted);

    System.out.println("ClearText: " + clearText);
    System.out.println("Decrypted: " + new String(decrypted, StandardCharsets.UTF_8));
    System.out.println("ClearText length: " + clearText.getBytes(StandardCharsets.UTF_8).length);
    System.out.println("Encrypted length: " + encrypted.length);
    System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));
}

输出如下所示:

ClearText: Sample plain text
Decrypted: Sample plain text
ClearText length: 17
Encrypted length: 256
Encrypted: riHHycTvKaDtX3SkeoZbFCW3KW3vxEIsF3wVQqOKuwAbTtWFyP6yN5essem+jTx16Ggdp6/rzS9r9Wy5O6P8JuOQAKi...

您可以看到明文已经膨胀多达256个字节。这是因为我生成了一个2048位长度的RSA密钥,这是256字节。RSA密码以块为单位加密,并在块中使用填充。我使用“PKCS1Padding”,它使用11个字节进行填充,这意味着,最多可以在一个块中加密256-11 = 245字节的普通数据。如果您有更大的数据要加密,则需要链接这些块。链接块有不同的方法:电子码本(ECB),密码块链接(CBC)。请参阅 分组密码操作模式。您还可以考虑使用混合方法,这意味着您将首先通过RSA交换AES的对称密钥,然后将此AES密钥用于要交换的较大数据。
另请参见:
mbedtls - ASN.1 DER和PEM中的关键结构
英文原文:Using openssl and java for RSA keys

About Me
后端开发工程师
GitHub Repos