How to write private key to password protected DER

2019-08-23 11:39发布

I have an RSA key pair that I generated in Java and I need to programmatically write the private key to the same format that openssl does when I run this command (and enter the appropriate data for the prompts, namely a passphrase to protect the private key):

openssl req -out request.csr -newkey rsa:2048 -keyout privkeyfile

The Java code to generate the key pair is pretty standard:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.genKeyPair();

A sample output of running that openssl command (on my Windows machine) is:

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI1c9lGdEA388CAggA
MBQGCCqGSIb3DQMHBAgKdWZEOyS2XgSCBMgI7b/vAeE6yz136BZkOzOPLv0uTz/Z
5mP4xO8IdAybE+PHJ71Mro4kgz+EMN39dk0ZWxbNnPpHGD+a6LfNKxos8fJL+dbz
dgc7I4fH9UQFLnYM64Xmq4aG66fIehuhqXBUUru+PJdBf5bfPDJDYAVEUsZ2J8bW
n4pLeS62Orwe+hhe/i/Y4gmgGAxGhUlDGc7T/N4RvhWUVNXQKGCGynj9Mv+LRzW6
rkGbBALQAKnj2tuihGSKhhR3WoNxTFqU9HsRGkzbJ5AiYhyBk7ObQw285TyIQS6k
OH25byIeaqzQ3Xn/wB6VrOQrsCvbWim1DZEIGRp6B+RKd0vUrkxFZRBsLdGXN1w/
bCM4dmhUJng2O+a9tSif7CC0emJqXgvkE2lGA9RMZltfOK+Kohi4L0WKxIX8TQqP
KzDhecSOaOkdXI0Tpho1QZDS6D+nvN2OiXlswgB0By3pkLdx4j7ZtjhqH/cg4rlE
8R1HzcilIrSlMK579UNGieis2wHaWobeinJqP6ruHK3HiAvG/WLFQ4TKexFa4/gy
EdXPaV9owRi+9nyRZGT8NsfzUDg5oTBLcg08uOlNHr8z8pF6a7l2sr4bzgcYFlko
BCIunMJpXYp/lUnL9daElbOPbGgeLNa8KfU7tXnzYsCg3iUx79fQUoql2pn2wMc3
0vQVTZ7/Enzl8cM2srl0uf1JMxMGOJ2kbdYZ8VwxaaMHnghN97eBsp+aRFCuAN4x
+D90ABBxRcBwzBOf8sT77vYXvQZNqUnzl5GJh2hlXCB5upNFqbSGaa6Yk+y4cw5e
3tB3/BHwZop2AAnPexnnQuCsn+SpCiLF+/agMouph61oWWJYQMwmUemNy/5G6AoP
KdBGqBAXonRSk8pBNqglHl0GOiBITl45+Bk4JBGM6+NcEpQ8B3OA+Vkj0n/aF/Iw
66Fo+UyA64fboC3q6DLxHZuTAY/giytwUW2QM4yFkEOm1v1WisTf0MO3Zt+ghuBn
8DG9MXGxP0XA9QzHAjCcDD8DK/hXsxaBg6xOV4bV+HhhJXsyWQAqcqKQro9Ik3L9
YvdJNU9BWyzKV79j5gYkDgLZgcA8QrGDArFZ5Hr9HdepBu8Njk09YDKJsfVMmk4E
6NbzxqHgPyYY3QtANLKg3EImBfuRHwfgfbaamrmYE0fSyh/QJMK0zDtDpkgiiTre
A8b16rBdBxZBSaO/J+Oje0pePLBRRhwX4WxcPsZeN5fO6S2NTECrWsf0jDG4D6pa
cannasXB4LoBifAYhKKTXFbQRY74wOxVfI7gw0qEjB7Jb1M2zCMwddgumOiCzGpu
d9voABJdGMdhwZ/FLbuxcr0y3p8Y5N9vW8ffSlxEtvhbPlszpgTPi2WWTNE+wTUQ
so4cvWFq9SP3Mg6Te6AStjdN1Mnhj2fb7ogxa5rsNxVrE/guRVgly4i9vG7Mi2Wd
bhT1vQypyL9g97nq0rRznDAjAtLenOagK4h+WJgZN2RpUhkWmO1trLGao/PrhgvD
8mOMCnZIQGMk5vS55druRoakPjsx4yZpzZvw5gPBXJ0H1KmbFUO1aSy/6N4nVBW+
Khr+ZHxboPD0zxJMzANjuOIJ/C46Hx5Wb/VP49NDmOLzLAi3+YSAhi3PB9D8vzxQ
MwM=
-----END ENCRYPTED PRIVATE KEY-----

EDIT Changed the sample output from openssl

EDIT I tried to read the openssl generated private key file with Java using the code below to try and get some of the parameters but I ended up getting the following exception:

Exception in thread "main" java.io.IOException: ObjectIdentifier() -- data isn't an object ID (tag = 48)
    at sun.security.util.ObjectIdentifier.<init>(Unknown Source)
    at sun.security.util.DerInputStream.getOID(Unknown Source)
    at com.sun.crypto.provider.PBES2Parameters.engineInit(PBES2Parameters.java:267)
    at java.security.AlgorithmParameters.init(Unknown Source)
    at sun.security.x509.AlgorithmId.decodeParams(Unknown Source)
    at sun.security.x509.AlgorithmId.<init>(Unknown Source)
    at sun.security.x509.AlgorithmId.parse(Unknown Source)
    at javax.crypto.EncryptedPrivateKeyInfo.<init>(EncryptedPrivateKeyInfo.java:95)
    at crypto.ReadOpensslKey.main(ReadOpensslKey.java:35)

Java code to read file:

package crypto;

import org.bouncycastle.util.encoders.Base64;
import javax.crypto.EncryptedPrivateKeyInfo;  
import javax.crypto.SecretKeyFactory;  
import javax.crypto.spec.PBEKeySpec;  
import java.io.IOException;  
import java.nio.file.Files;  
import java.nio.file.Paths;  
import java.security.InvalidKeyException;  
import java.security.KeyFactory;  
import java.security.NoSuchAlgorithmException;  
import java.security.PrivateKey;  
import java.security.spec.InvalidKeySpecException;  
import java.security.spec.PKCS8EncodedKeySpec; 

public class ReadOpensslKey {

    public static void main(String[] args) throws Exception {

        String encrypted = new String(Files.readAllBytes(Paths.get("<insert path to openssl generated privkeyfile>")));  

        //Create object from encrypted private key  
        encrypted = encrypted.replace("-----BEGIN ENCRYPTED PRIVATE KEY-----", "");  
        encrypted = encrypted.replace("-----END ENCRYPTED PRIVATE KEY-----", "");  
        EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(Base64.decode(encrypted));  // exception is thrown here
        System.out.println(pkInfo.getAlgName());
        PBEKeySpec keySpec = new PBEKeySpec("abcde".toCharArray()); // password  
        SecretKeyFactory pbeKeyFactory = SecretKeyFactory.getInstance(pkInfo.getAlgName());  
        PKCS8EncodedKeySpec encodedKeySpec = pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));  
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");  
        PrivateKey encryptedPrivateKey = keyFactory.generatePrivate(encodedKeySpec);  
    }

}

2条回答
够拽才男人
2楼-- · 2019-08-23 12:26

First, do you really need this specific format, or just a format OpenSSL (and programs using OpenSSL like Apache httpd and nginx and curl and PHP and many more) can use? If the latter, there are several other options that are easier and better. But you didn't ask that, so I won't answer it.

Second, you must have a really old OpenSSL. Since release 1.0.0 in 2010, req -newkey -keyout writes PKCS8 format, not traditional aka legacy format.

Third, this format is PEM not DER; there is a traditional DER format but it cannot be encrypted. (PKCS8 can be encrypted in either DER or PEM.)

Fourth, if you can use BouncyCastle, it can do this directly; from (any recent version of) bcpkix use org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator on your standard-JCE PrivateKey and a JcePEMEncryptorBuilder specifying DES-EDE3-CBC to create a PEMObject in memory, and then PEMWriter to write that out. Even if you can't actually use BC, it is open source (and pretty well designed IMO, though mostly lightly commented) and it may help you to look at their code.

Those said, what you asked is documented (barely) by the man page for PEM_write_RSAPrivateKey (which should be on your system but since your version is old you might better use the web copy) at the section headed 'PEM ENCRYPTION FORMAT' near the end, combined with the referenced man page for EVP_BytesToKey. Specifically:

  • construct the 'traditional' encoding which is RSAPrivateKey from PKCS1 (currently rfc8017), not the PKCS8/rfc5208 PrivateKeyInfo encoding returned by JCE PrivateKey.getEncoded(). The PKCS8 encoding does include the PKCS1 encoding as a portion (PKCS8 is an algorithm-generic wrapper around any number of algorithm-specific encodings) so you can either extract the PKCS1 from the PKCS8 (as BC does) or construct the PKCS1 directly from the key components n,e,d,p,q,dmp1,dmq1,qinvp. (The otherPrimeInfos is only for so-called 'multiprime' RSA which means more than 2 factors of n and which almost noone actually uses.)

  • derive the actual encryption key from the password using one(!) iteration of MD5 applied to password||salt where salt is a copy of the random IV (8-bytes for 3DES) plus (since that is not enough) MD5(firstblock||password||salt) then truncate the total to 24 bytes.

  • encrypt with 3DES (which JCE calls DESEDE) with CBC (with IV as above) and PKCS5 padding. (For 3DES or DES the low bit of each byte of key is nominally parity, but you don't need to set them because JCE doesn't implement them.)

  • convert to base64 with linebreaks every 64 characters, and add the BEGIN and END lines and the header lines including, as you correctly guessed, the IV in hex

查看更多
手持菜刀,她持情操
3楼-- · 2019-08-23 12:26

I ended up getting it to work using the following code:

import java.io.FileOutputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PKCS8Generator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
import org.bouncycastle.operator.OutputEncryptor;
import org.bouncycastle.util.io.pem.PemObject; 

public class WriteOpensslKey {

    public static void main(String[] args) throws Exception {

        // provider is needed for the encryptor builder
        Security.addProvider(new BouncyCastleProvider());

        // generate key pair
        KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");  
        kpGen.initialize(2048, new SecureRandom());  
        KeyPair keyPair = kpGen.generateKeyPair();  

        // construct encryptor builder to encrypt the private key
        JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC);  
        encryptorBuilder.setRandom(new SecureRandom());  
        encryptorBuilder.setPasssword("password".toCharArray());
        OutputEncryptor encryptor = encryptorBuilder.build();  

        // construct object to create the PKCS8 object from the private key and encryptor
        JcaPKCS8Generator pkcsGenerator = new JcaPKCS8Generator(keyPair.getPrivate(), encryptor);  
        PemObject pemObj = pkcsGenerator.generate();  
        StringWriter stringWriter = new StringWriter();  
        try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {  
            pemWriter.writeObject(pemObj);  
        }  

        // write PKCS8 to file
        String pkcs8Key = stringWriter.toString();  
        FileOutputStream fos = new FileOutputStream("<path to output file>");  
        fos.write(pkcs8Key.getBytes(StandardCharsets.UTF_8));  
        fos.flush();  
        fos.close();  

    }

}

I was then able to use this private key with openssl to sign a file as a quick test that it works.

Huge thanks to @dave_thompson_085 for pointing me in the right direction!

查看更多
登录 后发表回答