Search code examples
javaencryptionrsaprivate-key

Java 21: Generate encrypted private RSA key in PEM format


I'd like to generate an RSA key pair in Java 21 using plain Java API (without using BouncyCastle or similar libraries). The private key should be processable by both OpenSSL and Java. Based on Java limitations, this means that I'd like to generate an AES-encrypted private key.

The code below works fine for generating unencrypted private keys, but despite many Google & StackOverflow searches, I haven't been able to successfully implement the pemEncrypt method. I'm getting somewhat lost in the many algorithms and formats; maybe I need to do extra work for generating a proper ASN.1 data structure?

   @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public static final class KPGenerator {
        private final char[] passPhrase;
        
        @SneakyThrows
        public final KeyPair generate() {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048); 
            return kpg.generateKeyPair();
        }
        
        @SneakyThrows
        public final void writePem(Path privateKeyPath, Path publicKeyPath) {
            var kp = generate();
            writePem("PRIVATE KEY", kp.getPrivate().getEncoded(), privateKeyPath);
            writePem("PUBLIC KEY", kp.getPublic().getEncoded(), publicKeyPath);
        }
        
        @SneakyThrows
        private final void writePem(String type, byte[] key, Path path) {
            var pemString = asPem(type, key);
            Files.writeString(path, pemString, StandardOpenOption.CREATE_NEW);
        }
        
        private final String asPem(String type, byte[] key) {
            if ( "PRIVATE KEY".equals(type) && passPhrase!=null ) {
                return asPem("ENCRYPTED PRIVATE KEY", pemEncrypt(key, passPhrase));
            }
            return "-----BEGIN "+type+"-----\n"
                    + Base64.getMimeEncoder().encodeToString(key)
                    + "\n-----END "+type+"-----";
        }
        
        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
           ???
        }
}

Below are some examples of pemEncrypt implementations that I've tried.

Method 1

Based on https://medium.com/@patc888/decrypt-openssl-encrypted-data-in-java-4c31983afe19

Problem: openssl asn1parse -in privatekey.pem throws an error: 4047F272597F0000:error:0680009B:asn1 encoding routines:ASN1_get_object:too long:../crypto/asn1/asn1_lib.c:95

As written in the comments, this approach is wrong.

        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
            var salt = createSalt();
            byte[] passAndSalt = ArrayUtils.addAll(toBytes(passPhrase), salt);
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] key = md.digest(passAndSalt);
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
            md.reset();
            byte[] iv = Arrays.copyOfRange(md.digest(ArrayUtils.addAll(key, passAndSalt)), 0, 16);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
            byte[] encryptedSecretKey = cipher.doFinal(privateKey);
            try ( var bos = new ByteArrayOutputStream(); ) {
                bos.writeBytes("Salted__".getBytes(StandardCharsets.US_ASCII));
                bos.writeBytes(salt);
                bos.writeBytes(encryptedSecretKey);
                return bos.toByteArray();
            }
        }
        private static final byte[] toBytes(char[] chars) {
            CharBuffer charBuffer = CharBuffer.wrap(chars);
            ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
            byte[] bytes = Arrays.copyOfRange(byteBuffer.array(),
                      byteBuffer.position(), byteBuffer.limit());
            Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
            return bytes;
        }
        private static final byte[] createSalt() {
            final byte[] salt = new byte[16];
            new SecureRandom().nextBytes(salt);
            return salt;
        }

(sample output removed, not relevant as this approach is wrong)

Method 2

Problem: Similar OpenSSL error as before: 40B70A48E47F0000:error:0680007B:asn1 encoding routines:ASN1_get_object:header too long:../crypto/asn1/asn1_lib.c:105:

As written in the comments, this approach is wrong.

        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] key, char[] passPhrase) {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, createKey(passPhrase), createIv());
            return cipher.doFinal(key);
        }
        @SneakyThrows
        private static final SecretKey createKey(char[] passPhrase) {
            //PBE results in invalid key length exception
            //SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec spec = new PBEKeySpec(passPhrase, createSalt(), 65536, 256);
            SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
            return secret;
        }
        
        private static final IvParameterSpec createIv() {
            final byte[] iv = new byte[16];
            new SecureRandom().nextBytes(iv);
            return new IvParameterSpec(iv);
        }
        
        private static final byte[] createSalt() {
            final byte[] salt = new byte[16];
            new SecureRandom().nextBytes(salt);
            return salt;
        }

(sample output removed, not relevant as this approach is wrong)

Method 3

Based on ChatGPT.

Problem: Java exception java.security.InvalidKeyException: Wrong algorithm: AES or Rijndael required on Cipher.init()

        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey);
            byte[] salt = createSalt();
            int iterationCount = 65536; // You can adjust this value
            int keyLength = 128; // You can adjust this value
    
            PBEKeySpec pbeKeySpec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
    
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedKey = cipher.doFinal(pkcs8EncodedKeySpec.getEncoded());
    
            EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(cipher.getParameters(), encryptedKey);
            return encryptedPrivateKeyInfo.getEncoded();
        }

Method 4

Based on the comment below about using an existing OpenSSL-generated private key as a template. This fails with the following exception on the last line of the pemEncrypt method:

 java.security.NoSuchAlgorithmException: unrecognized algorithm name: PBEWithHmacSHA256AndAES_256
    at java.base/sun.security.x509.AlgorithmId.get(AlgorithmId.java:477)
    at java.base/javax.crypto.EncryptedPrivateKeyInfo.<init>(EncryptedPrivateKeyInfo.java:191)
        @SneakyThrows
        private static byte[] pemEncrypt(byte[] key, char[] passPhrase) {
            EncryptedPrivateKeyInfo encryptPKInfo = new EncryptedPrivateKeyInfo(readTemplate());
            Cipher cipher = Cipher.getInstance(encryptPKInfo.getAlgName());
            PBEKeySpec pbeKeySpec = new PBEKeySpec(passPhrase);
            SecretKeyFactory secFac = SecretKeyFactory.getInstance(encryptPKInfo.getAlgName());
            Key pbeKey = secFac.generateSecret(pbeKeySpec);
            // For now using original parameters; 
            // I suppose I should create new parameters instead?
            AlgorithmParameters algParams = encryptPKInfo.getAlgParameters();
            cipher.init(Cipher.ENCRYPT_MODE, pbeKey, algParams);
            byte[] body = cipher.doFinal(key);
            return new EncryptedPrivateKeyInfo(cipher.getParameters(), body).getEncoded();
        }
        
        @SneakyThrows
        private static byte[] readTemplate() {
            try ( var is = KPGenerator.class.getClassLoader().getResourceAsStream("com/company/crypto/key-template.aes256.pem") ) {
                return getKey(IOUtils.toString(is, StandardCharsets.US_ASCII));
            }
        }

        private static final byte[] getKey(String pemOrBase64Key) {
            var base64 = pemOrBase64Key.replaceAll("-----(BEGIN|END) [\\sA-Z]+ KEY-----|\\s", "");
            return Base64.getDecoder().decode(base64);
        }

Solution

  • The solution of the other answer is efficient and flexible. For earlier Java versions or unsupported algorithms such as PBES2/PBKDF2/AES-CBC, a solution can be found using third-party libraries such as BouncyCastle.

    However, if, as is the case with your requirements, these should/may not be used, a pragmatic workaround is to use an encrypted PKCS#8 key as a template in which the components (salt, IV, iteration count, ciphertext etc.) are replaced by the actual values.
    However, this solution is less flexible, as the parameters can only be varied within the limits defined by the template, but is on the other hand comparatively easy to implement (i.e. without an ASN.1 decoder/encoder API).

    In the following, the encryption of a PKCS#8 key is implemented, as it is also generated by the following OpenSSL statement:

    openssl genpkey -algorithm rsa -out encPkcs8 -aes-256-cbc -pass pass:mypassphrase 
    

    This generates, e.g. the following Base64 encoded, DER encoded encrypted PKCS#8 key:

    MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIwg/JX+PnPj4CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCCVo3Z4HqSdiDBtSLO9wx/BIIE0M4I9IP4txTnuBtQNK6tDh2SlXZgSvAHOz0S9Bwkf8YBZLNvJDF8ceB08nPp0b1qL7HqAvXsZYSwKIXGjftjs00FCNq4luJOaH5Z6kRGMQobKdKP2NvXwhlvT8v8vd2AH55x1s5jxsKQELEyQZP0fYdh2xAUCJ0Zat1LVubgNu5ecG5vOzEEQGQQLc64HcKgvr5zM4a6mthvHyGnwU6x0kYulJLAjAMAoRaJt4pCGtUdWIyS7w66YtzJr2bDO2l8zkxAcP/NX0XdyxH7aTG1ipK6Gy+p0a6plo+I/Q26jTlCYFd44HQK2SDSMXmun5i1IwN66oXzWs1g8DinUfFCOCWQ9BY9xVWHoLnL2AqnCtv1r4MgBw+UqxWjm8vGmb3CeBW98roiwviMaa/v/yYIhPUiXMM74zelQsCW8LuEvTf/Kb8GO132AfSvgXKkda/QOtVopC7ZO43GeeLSgvNF6bZDCtwRmcrZeDk0hW9Z+/znCdJuKXihsiy8SRJ3+xMorJYj6NJ6o13UzUzl/z300iDU0T8aAK/6UY5S5UATIpVYKiUv8y4C+EevAmJBVrBlgm74h/c9NE/ToTJir1wRl+7eiEBGyCXaa1mg7vvGajLnW4UcX38/caWLcro1CWb6ncX4SFhN5Y3+3Dt4rRzpRqqefRY0lvHlUcHJmBWDivOSp6p6FqGzD2M0BfKJRwf2q+fltWMg8jhXdpaGDS5X00+o1HFGg5SRp4rwKGTet6DGKG4P2ZuQKhiOIgGFyBvDVPeeHrwCs4cMkv9LhlqvUVdOrl2YB7j+JKk3RAwbnGpY4bHVuzGi2Sj9kFKDG4EAMB3KiSOub3AE5PO0ltt0zl0hBUd5Jr8KBwK96LDr8KsYo/QNJLAkwFWhCGKBmHJ4q2Cm73aw/U6ZHipm/L1wBIC30jJlQbN2wRAaN8lEQ1LWGL8LWhYiF1AVn7rhGeFaQzJBu1S35bcEI25xUu6rG11sJMBgzVf3MICqQu5yQKPx2tAcizeBlhNPASjfrqh1otWER/PKOxaz3lBnnmgtcOcPtt0d7f5mS1ScvBma+axL/vOkZi3vPVPkWnAE7c3j4Xxl4iiguMuhvNd4OLLn1SqHvQ8OOrB/MLNRw6QMDMRQvousT0TBzKBOGzXGj/Rx9XVqs/0Q/qYfAv3e2h9+wlGCcun8Qx1/NbaDLv2vPNIXh2LjTi01ttS6Dxk/AI+VD8XafZtFlRdC50RxLi5n0jNnlSoRJBPzm/Ffp896TJzRSRQAq+sFfbmIKUJjIvcp2tTSjQh59jAr22mI9tuDCMq4V6Xrb9R80bdK3qkKgMMM2yqDTLZSH+Z2lLzL/gGuKa4oIZQblk4sY8OVTc7Pvph1dL1oVVHRRz/9HgTQkMKa2MY+s4nRjlR+6kUYW0rX/SjMpEbcvt1o2o+2QXvkquikV1UeF36NXQDgCaGvmbslLvnAL03NoGiS3Fa956+t4JRonNgpKRhDNUVoiDMsHgbJ0iUiEL0GM7xHA4l+0JBHk1m8wBepwzEZ1MNbMURF2oDWikn0U0m89OLRwKXEEmMytVzzvj8laxD8x/9LGcW/sUn4vBBA3hJTFi4SBUQgKRiMOEn3Dyc6/jdrHJmGgPAjXD2PC6Z4GB8zTAQ5UfQV
    

    which looks as follows in an ASN.1/DER parser:

    enter image description here

    This encrypted PKCS#8 key can be used as a template to create other encrypted PKCS#8 keys that are generated using AES-256/CBC, the PBKDF2/HMAC-SHA256 key derivation function with an 8-byte salt and an iteration count that can be stored on 2 bytes.

    The encrypted data can be generated in a similar way to your method 3:

    private static byte[] encryptKey(byte[] pkcs8der, byte[] iv, char[] passphrase, byte[] salt, int count) throws Exception {
        PBEKeySpec pbeKeySpec = new PBEKeySpec(passphrase, salt, count, 256); // AES key size predefined by the template
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance( "PBKDF2WithHmacSHA256"); // KDF predefined by the template
        SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretKey.getEncoded(), "AES"), new IvParameterSpec(iv));
        return cipher.doFinal(pkcs8der);
    }
    

    Then the actual values are inserted into the template (the positions for the replacements must be determined once, which is most conveniently done using the ASN.1/DER parser):

    private static byte[] encryptKey(byte[] pkcs8enc, char[] passphrase) throws Exception {     
        int count = 10000; // 1 - 32767; max count predefined by the template       
        // generate random salt and IV
        SecureRandom secureRandom = new SecureRandom();
        byte[] salt = new byte[8]; // salt size predefined by the template
        secureRandom.nextBytes(salt); 
        byte[] iv = new byte[16]; 
        secureRandom.nextBytes(iv);         
        // encrypt key (PBKDF2/HMAC-SHA256, AES-256/CBC)
        byte[] ciphertext = encryptKey(pkcs8enc, iv, passphrase, salt, count);      
        // replace byte sequences in template
        byte[] encryptedPkcs8Der = Base64.getDecoder().decode("MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIwg/JX+PnPj4CAggAMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCCVo3Z4HqSdiDBtSLO9wx/BIIE0M4I9IP4txTnuBtQNK6tDh2SlXZgSvAHOz0S9Bwkf8YBZLNvJDF8ceB08nPp0b1qL7HqAvXsZYSwKIXGjftjs00FCNq4luJOaH5Z6kRGMQobKdKP2NvXwhlvT8v8vd2AH55x1s5jxsKQELEyQZP0fYdh2xAUCJ0Zat1LVubgNu5ecG5vOzEEQGQQLc64HcKgvr5zM4a6mthvHyGnwU6x0kYulJLAjAMAoRaJt4pCGtUdWIyS7w66YtzJr2bDO2l8zkxAcP/NX0XdyxH7aTG1ipK6Gy+p0a6plo+I/Q26jTlCYFd44HQK2SDSMXmun5i1IwN66oXzWs1g8DinUfFCOCWQ9BY9xVWHoLnL2AqnCtv1r4MgBw+UqxWjm8vGmb3CeBW98roiwviMaa/v/yYIhPUiXMM74zelQsCW8LuEvTf/Kb8GO132AfSvgXKkda/QOtVopC7ZO43GeeLSgvNF6bZDCtwRmcrZeDk0hW9Z+/znCdJuKXihsiy8SRJ3+xMorJYj6NJ6o13UzUzl/z300iDU0T8aAK/6UY5S5UATIpVYKiUv8y4C+EevAmJBVrBlgm74h/c9NE/ToTJir1wRl+7eiEBGyCXaa1mg7vvGajLnW4UcX38/caWLcro1CWb6ncX4SFhN5Y3+3Dt4rRzpRqqefRY0lvHlUcHJmBWDivOSp6p6FqGzD2M0BfKJRwf2q+fltWMg8jhXdpaGDS5X00+o1HFGg5SRp4rwKGTet6DGKG4P2ZuQKhiOIgGFyBvDVPeeHrwCs4cMkv9LhlqvUVdOrl2YB7j+JKk3RAwbnGpY4bHVuzGi2Sj9kFKDG4EAMB3KiSOub3AE5PO0ltt0zl0hBUd5Jr8KBwK96LDr8KsYo/QNJLAkwFWhCGKBmHJ4q2Cm73aw/U6ZHipm/L1wBIC30jJlQbN2wRAaN8lEQ1LWGL8LWhYiF1AVn7rhGeFaQzJBu1S35bcEI25xUu6rG11sJMBgzVf3MICqQu5yQKPx2tAcizeBlhNPASjfrqh1otWER/PKOxaz3lBnnmgtcOcPtt0d7f5mS1ScvBma+axL/vOkZi3vPVPkWnAE7c3j4Xxl4iiguMuhvNd4OLLn1SqHvQ8OOrB/MLNRw6QMDMRQvousT0TBzKBOGzXGj/Rx9XVqs/0Q/qYfAv3e2h9+wlGCcun8Qx1/NbaDLv2vPNIXh2LjTi01ttS6Dxk/AI+VD8XafZtFlRdC50RxLi5n0jNnlSoRJBPzm/Ffp896TJzRSRQAq+sFfbmIKUJjIvcp2tTSjQh59jAr22mI9tuDCMq4V6Xrb9R80bdK3qkKgMMM2yqDTLZSH+Z2lLzL/gGuKa4oIZQblk4sY8OVTc7Pvph1dL1oVVHRRz/9HgTQkMKa2MY+s4nRjlR+6kUYW0rX/SjMpEbcvt1o2o+2QXvkquikV1UeF36NXQDgCaGvmbslLvnAL03NoGiS3Fa956+t4JRonNgpKRhDNUVoiDMsHgbJ0iUiEL0GM7xHA4l+0JBHk1m8wBepwzEZ1MNbMURF2oDWikn0U0m89OLRwKXEEmMytVzzvj8laxD8x/9LGcW/sUn4vBBA3hJTFi4SBUQgKRiMOEn3Dyc6/jdrHJmGgPAjXD2PC6Z4GB8zTAQ5UfQV");
        System.arraycopy(salt, 0, encryptedPkcs8Der, 34+2, salt.length);
        System.arraycopy(iv, 0, encryptedPkcs8Der, 75+2, iv.length);
        byte[] count_bytes = ByteBuffer.allocate(4).putInt(count).array(); 
        System.arraycopy(count_bytes, 2, encryptedPkcs8Der, 44+2, count_bytes.length - 2);
        System.arraycopy(ciphertext, 0, encryptedPkcs8Der, 93+4, ciphertext.length);    
        return encryptedPkcs8Der;
    }
    

    Example of use:

    private static void encryptKey() throws Exception {
        char[] passphrase = "mypassphrase".toCharArray();
        byte[] pkcs8Der = Base64.getDecoder().decode("MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6cXloNrocJ8swwj8xktPmEOTfTgJT7KkUwWIGOjBB1QxApgdn5+SUHsakvEJq3Fgn+FnuuN8cfcqWrbT9jURtgNGinJNJ+StPM/PCxfhSv+XbkK11CV2EcJMyDB/8S/9u4E2ht/N1kT4OF2/mVDAq2MjjeUMq8CLmQR63ZMXB8lwmsGJEl4Rwt9WBZNcZFCfuCeBYrKRS7mtLzd4BTXEf0UuiNB/KJrz38TKSI47v/dRbB34wBNn0cuNLHb8t/eDaOvzV6J8SZgOWuL85mng6Fm77QGjUteWgJN76+YhDZgJfsRq1Q67JAy3ZXDHi5X538DcM/o+0wYEqkXxK3iIbAgMBAAECggEASlJj0ExIomKmmBhG8q8SM1s2sWG6gdQMjs6MEeluRT/1c2v79cq2Dum5y/+UBl8x8TUKPKSLpCLs+GXkiVKgHXrFlqoN+OYQArG2EUWzuODwczdYPhhupBXwR3oX4g41k/BsYfQfZBVzBFEJdWrIDLyAUFWNlfdGIj2BTiAoySfyqmamvmW8bsvc8coiGlZ28UC85/Xqx9wOzjeGoRkCH7PcTMlc9F7SxSthwX/k1VBXmNOHa+HzGOgO/W3k1LDqJbq2wKjZTW3iVEg2VodjxgBLMm0MueSGoI6IuaZSPMyFEM3gGvC2+cDBI2SL/amhiTUa/VDlTVw/IKbSuar9uQKBgQDd76M0Po5Lqh8ZhQ3obhFqkfO5EBXy7HUL15cw51kVtwF6Gf/J2HNHjwsg9Nb0eJETTS6bbuVd9bn884JoRS986nVTFNZ4dnjEgKjjQ8GjfzdkpbUxsRLWiIxuOQSpIUZGdMi2ctTTtspvMsDsjRRYdYIQCe/SDsdHGT3vcUCybwKBgQDXDz6iVnY84Fh5iDDVrQOR4lYoxCL/ikCDJjC6y1mjR0eVFdBPQ4j1dDSPU9lahBLby0VyagQCDp/kxQOl0z2zBLRI4I8jUtz9/9KW6ze7U7dQJ7OTfumd5I97OyQOG9XZwKUkRgfyb/PAMBSUSLgosi38f+OC3IN3qlvHFzvxFQKBgQCITpUDEmSczih5qQGIvolN1cRF5j5Ey7t7gXbnXz+Umah7kJpMIvdyfMVOAXJABgi8PQwiBLM0ySXo2LpARjXLV8ilNUggBktYDNktc8DrJMgltayaj3HNd2IglD5rjfc2cKWRgOd7/GlKcHaTEnbreYhfR2sWrWLxJOyoMfuVWwKBgFalCbMV6qU0LfEo8aPlBN8ttVDPVNpntP4h0NgxPXgPK8Pg+gA1UWSy4MouGg/hzkdHaj9ifyLlCX598a5JoT4S0x/ZeVHd/LNI8mtjcRzD6cMde7gdFbpLb5NSjIAyrsIAX4hxvpnqiOYRePkVIz0iLGziiaMbfMwlkrxvm/LRAoGBALPRbtSbE2pPgvOHKHTGPr7gKbmsWVbOcQA8rG801T38W/UPe1XtynMEjzzQ29OaVeQwvUN9+DxFXJ6Yvwj6ih4Wdq109i7Oo1fDnMczOQN9DKch2eNAHrNSOMyLDCBm++wbyHAsS2T0VO8+gzLABviZm5AFCQWfke4LZo5mOS10");
        byte[] encryptedPkcs8Der = encryptKey(pkcs8Der, passphrase); // RSA key size predefined by the template     
        // output as PEM
        System.out.println("-----BEGIN ENCRYPTED PRIVATE KEY-----");
        System.out.println(Base64.getMimeEncoder().encodeToString(encryptedPkcs8Der));
        System.out.println("-----END ENCRYPTED PRIVATE KEY-----");
    }
    

    As a test, the encrypted PKCS#8 key generated in this way can be decrypted e.g. with:

    openssl pkcs8 -topk8 -nocrypt -in encPkcs8.pem -out pkcs8.pem -passin pass:mypassphrase 
    

    which results in the original key.