This is an automated email from the ASF dual-hosted git repository.

pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 3a363bbb7c6 NIFI-16409 Replace Bouncy Castle with JDK in 
StandardPrivateKeyService (#11744)
3a363bbb7c6 is described below

commit 3a363bbb7c6bf7b1f9ac78be3961168d46329e5c
Author: David Handermann <[email protected]>
AuthorDate: Thu Oct 1 12:15:20 2026 -0500

    NIFI-16409 Replace Bouncy Castle with JDK in StandardPrivateKeyService 
(#11744)
    
    * NIFI-16409 Replaced Bouncy Castle with JDK in StandardPrivateKeyService
    
    - Added StandardPrivateKeyReader with supported PEM formats limited to PKCS8
    - Added minimal DerElement parsing for reading Object Identifiers
    - Restricted Cipher Algorithm support to AES and 3DES
    - Removed undocumented support for PKCS1 and legacy OpenSSL encryption
    - Expanded Capability Description to highlight focus on PBES2 and lack of 
guarantees for PBES1
---
 .../nifi-key-service/pom.xml                       |   8 -
 .../key/service/StandardPrivateKeyService.java     |  13 +-
 .../reader/BouncyCastlePrivateKeyReader.java       | 115 --------
 .../apache/nifi/key/service/reader/DerElement.java | 250 ++++++++++++++++
 .../reader/EncryptedPrivateKeyDecryptor.java       | 172 +++++++++++
 .../service/reader/PrivateKeyCipherAlgorithm.java  |  88 ++++++
 .../key/service/reader/PrivateKeyException.java    |   4 +
 .../service/reader/StandardPrivateKeyReader.java   | 159 +++++++++++
 .../key/service/StandardPrivateKeyServiceTest.java |  90 +++---
 .../nifi/key/service/reader/DerElementTest.java    | 124 ++++++++
 .../reader/StandardPrivateKeyReaderTest.java       | 317 +++++++++++++++++++++
 11 files changed, 1182 insertions(+), 158 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
index f3c14f4c730..712cc92d205 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/pom.xml
@@ -26,13 +26,5 @@
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-key-service-api</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcpkix-jdk18on</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk18on</artifactId>
-        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
index af9e0427455..c05f6a85fce 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/StandardPrivateKeyService.java
@@ -30,8 +30,8 @@ import org.apache.nifi.context.PropertyContext;
 import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.key.service.api.PrivateKeyService;
-import org.apache.nifi.key.service.reader.BouncyCastlePrivateKeyReader;
 import org.apache.nifi.key.service.reader.PrivateKeyReader;
+import org.apache.nifi.key.service.reader.StandardPrivateKeyReader;
 import org.apache.nifi.migration.PropertyConfiguration;
 import org.apache.nifi.processor.util.StandardValidators;
 import org.apache.nifi.reporting.InitializationException;
@@ -52,7 +52,14 @@ import java.util.concurrent.atomic.AtomicReference;
  * Standard implementation of Private Key Service supporting encrypted or 
unencrypted sources
  */
 @Tags({"PEM", "PKCS8"})
-@CapabilityDescription("Private Key Service provides access to a Private Key 
loaded from configured sources")
+@CapabilityDescription(
+    """
+    Private Key Service provides access to a Private Key loaded from 
configured sources.
+    Supports PKCS8 Private Keys with PEM encoding and optional password-based 
encryption.
+    Encryption support is targeted to PBES2 and PBKDF2 as described in RFC 
8018 Section 6.2.
+    Encryption support for PBES1 is not guaranteed and depends on the 
configured Java Security Provider.
+    """
+)
 public class StandardPrivateKeyService extends AbstractControllerService 
implements PrivateKeyService {
     public static final PropertyDescriptor KEY_FILE = new 
PropertyDescriptor.Builder()
             .name("Key File")
@@ -85,7 +92,7 @@ public class StandardPrivateKeyService extends 
AbstractControllerService impleme
 
     private static final Charset KEY_CHARACTER_SET = StandardCharsets.US_ASCII;
 
-    private static final PrivateKeyReader PRIVATE_KEY_READER = new 
BouncyCastlePrivateKeyReader();
+    private static final PrivateKeyReader PRIVATE_KEY_READER = new 
StandardPrivateKeyReader();
 
     private final AtomicReference<PrivateKey> keyReference = new 
AtomicReference<>();
 
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java
deleted file mode 100644
index 0f1219b4e2d..00000000000
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/BouncyCastlePrivateKeyReader.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.key.service.reader;
-
-import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.openssl.PEMDecryptorProvider;
-import org.bouncycastle.openssl.PEMEncryptedKeyPair;
-import org.bouncycastle.openssl.PEMException;
-import org.bouncycastle.openssl.PEMKeyPair;
-import org.bouncycastle.openssl.PEMParser;
-import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
-import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
-import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
-import org.bouncycastle.operator.InputDecryptorProvider;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
-import org.bouncycastle.pkcs.PKCSException;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
-import java.security.PrivateKey;
-
-/**
- * Bouncy Castle implementation of Private Key Reader supporting PEM files
- */
-public class BouncyCastlePrivateKeyReader implements PrivateKeyReader {
-    private static final String INVALID_PEM = "Invalid PEM";
-
-    private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new 
BouncyCastleProvider();
-
-    /**
-     * Read Private Key using Bouncy Castle PEM Parser
-     *
-     * @param inputStream Key stream
-     * @param keyPassword Password
-     * @return Private Key
-     */
-    @Override
-    public PrivateKey readPrivateKey(final InputStream inputStream, final 
char[] keyPassword) {
-        try (final PEMParser parser = new PEMParser(new 
InputStreamReader(inputStream))) {
-            final Object object = parser.readObject();
-
-            final PrivateKeyInfo privateKeyInfo;
-
-            if (object instanceof final PrivateKeyInfo privateKeyInfoObj) {
-                privateKeyInfo = privateKeyInfoObj;
-            } else if (object instanceof final PKCS8EncryptedPrivateKeyInfo 
encryptedPrivateKeyInfo) {
-                privateKeyInfo = 
readEncryptedPrivateKey(encryptedPrivateKeyInfo, keyPassword);
-            } else if (object instanceof final PEMKeyPair pemKeyPair) {
-                privateKeyInfo = pemKeyPair.getPrivateKeyInfo();
-            } else if (object instanceof final PEMEncryptedKeyPair 
encryptedKeyPair) {
-                privateKeyInfo = readEncryptedPrivateKey(encryptedKeyPair, 
keyPassword);
-            } else {
-                final String objectType = object == null ? INVALID_PEM : 
object.getClass().getName();
-                final String message = String.format("Private Key [%s] not 
supported", objectType);
-                throw new IllegalArgumentException(message);
-            }
-
-            return convertPrivateKey(privateKeyInfo);
-        } catch (final IOException e) {
-            throw new UncheckedIOException("Read Private Key stream failed", 
e);
-        }
-    }
-
-    private PrivateKeyInfo readEncryptedPrivateKey(final 
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, final char[] keyPassword) 
{
-        try {
-            final InputDecryptorProvider provider = new 
JceOpenSSLPKCS8DecryptorProviderBuilder()
-                    .setProvider(BOUNCY_CASTLE_PROVIDER)
-                    .build(keyPassword);
-            return encryptedPrivateKeyInfo.decryptPrivateKeyInfo(provider);
-        } catch (final OperatorCreationException e) {
-            throw new PrivateKeyException("Preparing Private Key Decryption 
failed", e);
-        } catch (final PKCSException e) {
-            throw new PrivateKeyException("Decrypting Private Key failed", e);
-        }
-    }
-
-    private PrivateKeyInfo readEncryptedPrivateKey(final PEMEncryptedKeyPair 
encryptedKeyPair, final char[] keyPassword) {
-        final PEMDecryptorProvider provider = new 
JcePEMDecryptorProviderBuilder()
-                .setProvider(BOUNCY_CASTLE_PROVIDER)
-                .build(keyPassword);
-        try {
-            final PEMKeyPair pemKeyPair = 
encryptedKeyPair.decryptKeyPair(provider);
-            return pemKeyPair.getPrivateKeyInfo();
-        } catch (final IOException e) {
-            throw new PrivateKeyException("Decrypting Private Key Pair 
failed", e);
-        }
-    }
-
-    private PrivateKey convertPrivateKey(final PrivateKeyInfo privateKeyInfo) {
-        final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
-        try {
-            return converter.getPrivateKey(privateKeyInfo);
-        } catch (final PEMException e) {
-            throw new PrivateKeyException("Convert Private Key failed", e);
-        }
-    }
-}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/DerElement.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/DerElement.java
new file mode 100644
index 00000000000..29d728e9f76
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/DerElement.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * ASN.1 element encoded using Distinguished Encoding Rules limited to 
definite lengths and low tag numbers
+ */
+final class DerElement {
+    static final int INTEGER_TAG = 0x02;
+
+    static final int OCTET_STRING_TAG = 0x04;
+
+    static final int OBJECT_IDENTIFIER_TAG = 0x06;
+
+    static final int SEQUENCE_TAG = 0x30;
+
+    private static final int CONSTRUCTED_TAG_FLAG = 0x20;
+
+    private static final int HIGH_TAG_NUMBER_FORM = 0x1F;
+
+    private static final int LONG_FORM_LENGTH_FLAG = 0x80;
+
+    private static final int LENGTH_BYTES_MASK = 0x7F;
+
+    private static final int MAXIMUM_LENGTH_BYTES = 4;
+
+    private static final int BYTE_MASK = 0xFF;
+
+    private static final int BYTE_BITS = 8;
+
+    private static final int ARC_CONTINUATION_FLAG = 0x80;
+
+    private static final int ARC_VALUE_MASK = 0x7F;
+
+    private static final int ARC_VALUE_BITS = 7;
+
+    private static final long MAXIMUM_ARC_BEFORE_SHIFT = Long.MAX_VALUE >> 
ARC_VALUE_BITS;
+
+    private static final int FIRST_ARC_FACTOR = 40;
+
+    private static final long MAXIMUM_FIRST_ARC = 2;
+
+    private static final char ARC_SEPARATOR = '.';
+
+    private final int tag;
+
+    private final byte[] contents;
+
+    private DerElement(final int tag, final byte[] contents) {
+        this.tag = tag;
+        this.contents = contents;
+    }
+
+    /**
+     * Read DER element from encoded bytes containing exactly one element
+     *
+     * @param encoded DER encoded bytes
+     * @return DER element
+     */
+    static DerElement read(final byte[] encoded) {
+        final List<DerElement> elements = readElements(encoded);
+        if (elements.size() != 1) {
+            throw new PrivateKeyException("DER encoding contains [%d] elements 
instead of 1".formatted(elements.size()));
+        }
+
+        return elements.getFirst();
+    }
+
+    /**
+     * Get DER tag
+     *
+     * @return DER tag
+     */
+    int getTag() {
+        return tag;
+    }
+
+    /**
+     * Get elements contained in a constructed element such as a SEQUENCE
+     *
+     * @return Contained elements
+     */
+    List<DerElement> getElements() {
+        if ((tag & CONSTRUCTED_TAG_FLAG) == 0) {
+            throw new PrivateKeyException("DER tag [0x%02X] not 
constructed".formatted(tag));
+        }
+
+        return readElements(contents);
+    }
+
+    /**
+     * Get element at the specified position in a constructed element such as 
a SEQUENCE
+     *
+     * @param index Element position starting from zero
+     * @return Contained element
+     */
+    DerElement getElement(final int index) {
+        final List<DerElement> elements = getElements();
+        if (index >= elements.size()) {
+            throw new PrivateKeyException("DER tag [0x%02X] element [%d] not 
found".formatted(tag, index));
+        }
+
+        return elements.get(index);
+    }
+
+    /**
+     * Get INTEGER value
+     *
+     * @return Signed integer value
+     */
+    BigInteger getInteger() {
+        requireTag(INTEGER_TAG);
+        if (contents.length == 0) {
+            throw new PrivateKeyException("DER INTEGER contents not found");
+        }
+
+        return new BigInteger(contents);
+    }
+
+    /**
+     * Get OCTET STRING value
+     *
+     * @return Copy of bytes
+     */
+    byte[] getOctetString() {
+        requireTag(OCTET_STRING_TAG);
+        return contents.clone();
+    }
+
+    /**
+     * Get OBJECT IDENTIFIER value formatted as a dotted decimal string
+     *
+     * @return Object Identifier such as 1.2.840.113549.1.1.1
+     */
+    String getObjectIdentifier() {
+        requireTag(OBJECT_IDENTIFIER_TAG);
+        if (contents.length == 0) {
+            throw new PrivateKeyException("DER OBJECT IDENTIFIER contents not 
found");
+        }
+
+        if ((contents[contents.length - 1] & ARC_CONTINUATION_FLAG) == 
ARC_CONTINUATION_FLAG) {
+            throw new PrivateKeyException("DER OBJECT IDENTIFIER truncated");
+        }
+
+        final StringBuilder builder = new StringBuilder();
+        long arc = 0;
+        for (final byte encodedByte : contents) {
+            final int arcByte = encodedByte & BYTE_MASK;
+            if (arc == 0 && arcByte == ARC_CONTINUATION_FLAG) {
+                throw new PrivateKeyException("DER OBJECT IDENTIFIER arc 
encoding not minimal");
+            }
+
+            if (arc > MAXIMUM_ARC_BEFORE_SHIFT) {
+                throw new PrivateKeyException("DER OBJECT IDENTIFIER arc 
exceeds maximum value");
+            }
+
+            arc = (arc << ARC_VALUE_BITS) | (arcByte & ARC_VALUE_MASK);
+            if ((arcByte & ARC_CONTINUATION_FLAG) == 0) {
+                if (builder.isEmpty()) {
+                    final long firstArc = Math.min(arc / FIRST_ARC_FACTOR, 
MAXIMUM_FIRST_ARC);
+                    builder.append(firstArc).append(ARC_SEPARATOR).append(arc 
- firstArc * FIRST_ARC_FACTOR);
+                } else {
+                    builder.append(ARC_SEPARATOR).append(arc);
+                }
+
+                arc = 0;
+            }
+        }
+
+        return builder.toString();
+    }
+
+    private void requireTag(final int expectedTag) {
+        if (tag != expectedTag) {
+            throw new PrivateKeyException("DER tag [0x%02X] found instead of 
expected tag [0x%02X]".formatted(tag, expectedTag));
+        }
+    }
+
+    private static List<DerElement> readElements(final byte[] encoded) {
+        final List<DerElement> elements = new ArrayList<>();
+
+        int offset = 0;
+        while (offset < encoded.length) {
+            final int elementTag = encoded[offset] & BYTE_MASK;
+            if ((elementTag & HIGH_TAG_NUMBER_FORM) == HIGH_TAG_NUMBER_FORM) {
+                throw new PrivateKeyException("DER tag [0x%02X] high tag 
number form not supported".formatted(elementTag));
+            }
+
+            offset++;
+            if (offset == encoded.length) {
+                throw new PrivateKeyException("DER tag [0x%02X] length not 
found".formatted(elementTag));
+            }
+
+            final int initialLength = encoded[offset] & BYTE_MASK;
+            offset++;
+
+            final long length;
+            if ((initialLength & LONG_FORM_LENGTH_FLAG) == 0) {
+                length = initialLength;
+            } else {
+                final int lengthBytes = initialLength & LENGTH_BYTES_MASK;
+                if (lengthBytes == 0) {
+                    throw new PrivateKeyException("DER tag [0x%02X] indefinite 
length not supported".formatted(elementTag));
+                }
+
+                if (lengthBytes > MAXIMUM_LENGTH_BYTES || lengthBytes > 
encoded.length - offset) {
+                    throw new PrivateKeyException("DER tag [0x%02X] length 
bytes [%d] not valid".formatted(elementTag, lengthBytes));
+                }
+
+                long longFormLength = 0;
+                for (int lengthByte = 0; lengthByte < lengthBytes; 
lengthByte++) {
+                    longFormLength = (longFormLength << BYTE_BITS) | 
(encoded[offset] & BYTE_MASK);
+                    offset++;
+                }
+
+                length = longFormLength;
+            }
+
+            final int remainingBytes = encoded.length - offset;
+            if (length > remainingBytes) {
+                throw new PrivateKeyException("DER tag [0x%02X] length [%d] 
exceeds remaining bytes [%d]".formatted(elementTag, length, remainingBytes));
+            }
+
+            final int contentsLength = (int) length;
+            elements.add(new DerElement(elementTag, 
Arrays.copyOfRange(encoded, offset, offset + contentsLength)));
+            offset += contentsLength;
+        }
+
+        return elements;
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/EncryptedPrivateKeyDecryptor.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/EncryptedPrivateKeyDecryptor.java
new file mode 100644
index 00000000000..0273402252e
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/EncryptedPrivateKeyDecryptor.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Decryptor for PKCS8 Encrypted Private Key Information supporting PBES2 with 
PBKDF2 and other password-based encryption algorithms available from security 
providers
+ */
+class EncryptedPrivateKeyDecryptor {
+    private static final String PBES2_OBJECT_IDENTIFIER = 
"1.2.840.113549.1.5.13";
+
+    private static final String PBKDF2_OBJECT_IDENTIFIER = 
"1.2.840.113549.1.5.12";
+
+    private static final String DEFAULT_KEY_DERIVATION_ALGORITHM = 
"PBKDF2WithHmacSHA1";
+
+    private static final Map<String, String> KEY_DERIVATION_ALGORITHMS = 
Map.of(
+            "1.2.840.113549.2.7", DEFAULT_KEY_DERIVATION_ALGORITHM,
+            "1.2.840.113549.2.8", "PBKDF2WithHmacSHA224",
+            "1.2.840.113549.2.9", "PBKDF2WithHmacSHA256",
+            "1.2.840.113549.2.10", "PBKDF2WithHmacSHA384",
+            "1.2.840.113549.2.11", "PBKDF2WithHmacSHA512",
+            "1.2.840.113549.2.12", "PBKDF2WithHmacSHA512/224",
+            "1.2.840.113549.2.13", "PBKDF2WithHmacSHA512/256"
+    );
+
+    private static final int PBKDF2_OPTIONAL_PARAMETERS_INDEX = 2;
+
+    private static final int BYTE_BITS = 8;
+
+    private static final String CIPHER_TRANSFORMATION_FORMAT = 
"%s/CBC/PKCS5Padding";
+
+    /**
+     * Decrypt PKCS8 Encrypted Private Key Information
+     *
+     * @param encryptedPrivateKeyInfo DER encoded Encrypted Private Key 
Information
+     * @param keyPassword Password for decryption
+     * @return DER encoded PKCS8 Private Key Information
+     */
+    byte[] decrypt(final byte[] encryptedPrivateKeyInfo, final char[] 
keyPassword) {
+        final DerElement encryptedPrivateKeyInfoElement = 
DerElement.read(encryptedPrivateKeyInfo);
+        final DerElement encryptionAlgorithm = 
encryptedPrivateKeyInfoElement.getElement(0);
+        final String encryptionAlgorithmIdentifier = 
encryptionAlgorithm.getElement(0).getObjectIdentifier();
+
+        final byte[] privateKeyInfo;
+        if (PBES2_OBJECT_IDENTIFIER.equals(encryptionAlgorithmIdentifier)) {
+            final byte[] encryptedData = 
encryptedPrivateKeyInfoElement.getElement(1).getOctetString();
+            privateKeyInfo = decryptPbes2(encryptionAlgorithm.getElement(1), 
encryptedData, keyPassword);
+        } else {
+            privateKeyInfo = 
decryptPasswordBasedEncryption(encryptedPrivateKeyInfo, keyPassword);
+        }
+
+        return privateKeyInfo;
+    }
+
+    private byte[] decryptPbes2(final DerElement parameters, final byte[] 
encryptedData, final char[] keyPassword) {
+        final DerElement keyDerivationFunction = parameters.getElement(0);
+        final String keyDerivationFunctionIdentifier = 
keyDerivationFunction.getElement(0).getObjectIdentifier();
+        if (!PBKDF2_OBJECT_IDENTIFIER.equals(keyDerivationFunctionIdentifier)) 
{
+            throw new PrivateKeyException("PBES2 Key Derivation Function [%s] 
not supported".formatted(keyDerivationFunctionIdentifier));
+        }
+
+        final DerElement encryptionScheme = parameters.getElement(1);
+        final String encryptionSchemeIdentifier = 
encryptionScheme.getElement(0).getObjectIdentifier();
+        final PrivateKeyCipherAlgorithm cipherAlgorithm = 
PrivateKeyCipherAlgorithm.findByObjectIdentifier(encryptionSchemeIdentifier)
+                .orElseThrow(() -> new PrivateKeyException("PBES2 Encryption 
Scheme [%s] not supported".formatted(encryptionSchemeIdentifier)));
+        final byte[] initializationVector = 
encryptionScheme.getElement(1).getOctetString();
+
+        final byte[] derivedKey = 
deriveKey(keyDerivationFunction.getElement(1), keyPassword, 
cipherAlgorithm.getKeyLength());
+        try {
+            return decrypt(cipherAlgorithm, derivedKey, initializationVector, 
encryptedData);
+        } finally {
+            Arrays.fill(derivedKey, (byte) 0);
+        }
+    }
+
+    private byte[] deriveKey(final DerElement pbkdf2Parameters, final char[] 
keyPassword, final int keyLength) {
+        final byte[] salt = pbkdf2Parameters.getElement(0).getOctetString();
+        if (salt.length == 0) {
+            throw new PrivateKeyException("PBKDF2 Salt not found");
+        }
+
+        final BigInteger iterationCount = 
pbkdf2Parameters.getElement(1).getInteger();
+        if (iterationCount.signum() < 1 || iterationCount.bitLength() >= 
Integer.SIZE) {
+            throw new PrivateKeyException("PBKDF2 Iteration Count [%s] not 
valid".formatted(iterationCount));
+        }
+
+        String keyDerivationAlgorithm = DEFAULT_KEY_DERIVATION_ALGORITHM;
+        final List<DerElement> elements = pbkdf2Parameters.getElements();
+        for (final DerElement optionalParameter : 
elements.subList(PBKDF2_OPTIONAL_PARAMETERS_INDEX, elements.size())) {
+            if (optionalParameter.getTag() == DerElement.SEQUENCE_TAG) {
+                final String pseudoRandomFunctionIdentifier = 
optionalParameter.getElement(0).getObjectIdentifier();
+                keyDerivationAlgorithm = 
KEY_DERIVATION_ALGORITHMS.get(pseudoRandomFunctionIdentifier);
+                if (keyDerivationAlgorithm == null) {
+                    throw new PrivateKeyException("PBKDF2 Pseudorandom 
Function [%s] not supported".formatted(pseudoRandomFunctionIdentifier));
+                }
+            }
+        }
+
+        final PBEKeySpec keySpec = new PBEKeySpec(keyPassword, salt, 
iterationCount.intValue(), keyLength * BYTE_BITS);
+        try {
+            final SecretKeyFactory secretKeyFactory = 
SecretKeyFactory.getInstance(keyDerivationAlgorithm);
+            return secretKeyFactory.generateSecret(keySpec).getEncoded();
+        } catch (final GeneralSecurityException e) {
+            throw new PrivateKeyException("PBKDF2 Key Derivation with [%s] 
failed".formatted(keyDerivationAlgorithm), e);
+        } finally {
+            keySpec.clearPassword();
+        }
+    }
+
+    private byte[] decrypt(final PrivateKeyCipherAlgorithm cipherAlgorithm, 
final byte[] key, final byte[] initializationVector, final byte[] encrypted) {
+        final String keyAlgorithm = cipherAlgorithm.getKeyAlgorithm();
+        try {
+            final Cipher cipher = 
Cipher.getInstance(CIPHER_TRANSFORMATION_FORMAT.formatted(keyAlgorithm));
+            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, 
keyAlgorithm), new IvParameterSpec(initializationVector));
+            return cipher.doFinal(encrypted);
+        } catch (final GeneralSecurityException e) {
+            throw new PrivateKeyException("Decrypting Private Key with [%s] 
failed".formatted(cipherAlgorithm.getCipherName()), e);
+        }
+    }
+
+    private byte[] decryptPasswordBasedEncryption(final byte[] 
encryptedPrivateKeyInfo, final char[] keyPassword) {
+        final EncryptedPrivateKeyInfo privateKeyInfo;
+        try {
+            privateKeyInfo = new 
EncryptedPrivateKeyInfo(encryptedPrivateKeyInfo);
+        } catch (final IOException e) {
+            throw new PrivateKeyException("Encrypted Private Key parsing 
failed", e);
+        }
+
+        final String algorithm = privateKeyInfo.getAlgName();
+        final PBEKeySpec keySpec = new PBEKeySpec(keyPassword);
+        try {
+            final SecretKey secretKey = 
SecretKeyFactory.getInstance(algorithm).generateSecret(keySpec);
+            final Cipher cipher = Cipher.getInstance(algorithm);
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, 
privateKeyInfo.getAlgParameters());
+            return privateKeyInfo.getKeySpec(cipher).getEncoded();
+        } catch (final NoSuchAlgorithmException e) {
+            throw new PrivateKeyException("Encryption Algorithm [%s] not 
supported".formatted(algorithm), e);
+        } catch (final GeneralSecurityException e) {
+            throw new PrivateKeyException("Decrypting Private Key with [%s] 
failed".formatted(algorithm), e);
+        } finally {
+            keySpec.clearPassword();
+        }
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyCipherAlgorithm.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyCipherAlgorithm.java
new file mode 100644
index 00000000000..1d6a1a261ad
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyCipherAlgorithm.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import java.util.Optional;
+
+/**
+ * Cipher Algorithms using Cipher Block Chaining mode supported for PBES2 
Encryption Schemes
+ */
+enum PrivateKeyCipherAlgorithm {
+    AES_128_CBC("2.16.840.1.101.3.4.1.2", "AES-128-CBC", "AES", 16),
+
+    AES_256_CBC("2.16.840.1.101.3.4.1.42", "AES-256-CBC", "AES", 32),
+
+    DES_EDE3_CBC("1.2.840.113549.3.7", "DES-EDE3-CBC", "DESede", 24);
+
+    private final String objectIdentifier;
+
+    private final String cipherName;
+
+    private final String keyAlgorithm;
+
+    private final int keyLength;
+
+    PrivateKeyCipherAlgorithm(final String objectIdentifier, final String 
cipherName, final String keyAlgorithm, final int keyLength) {
+        this.objectIdentifier = objectIdentifier;
+        this.cipherName = cipherName;
+        this.keyAlgorithm = keyAlgorithm;
+        this.keyLength = keyLength;
+    }
+
+    /**
+     * Get cipher name for messages
+     *
+     * @return Cipher name such as AES-256-CBC
+     */
+    String getCipherName() {
+        return cipherName;
+    }
+
+    /**
+     * Get key algorithm for Secret Key and Cipher transformation
+     *
+     * @return Key algorithm such as AES
+     */
+    String getKeyAlgorithm() {
+        return keyAlgorithm;
+    }
+
+    /**
+     * Get key length in bytes
+     *
+     * @return Key length in bytes
+     */
+    int getKeyLength() {
+        return keyLength;
+    }
+
+    /**
+     * Find Cipher Algorithm using Object Identifier from PBES2 Encryption 
Scheme parameters
+     *
+     * @param objectIdentifier Object Identifier
+     * @return Cipher Algorithm or empty when not supported
+     */
+    static Optional<PrivateKeyCipherAlgorithm> findByObjectIdentifier(final 
String objectIdentifier) {
+        for (final PrivateKeyCipherAlgorithm cipherAlgorithm : values()) {
+            if (cipherAlgorithm.objectIdentifier.equals(objectIdentifier)) {
+                return Optional.of(cipherAlgorithm);
+            }
+        }
+
+        return Optional.empty();
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
index 01d6964d43f..733800a1359 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/PrivateKeyException.java
@@ -22,6 +22,10 @@ package org.apache.nifi.key.service.reader;
 public class PrivateKeyException extends RuntimeException {
     private static final long serialVersionUID = 1L;
 
+    public PrivateKeyException(final String message) {
+        super(message);
+    }
+
     public PrivateKeyException(final String message, final Throwable cause) {
         super(message, cause);
     }
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReader.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReader.java
new file mode 100644
index 00000000000..10967e7f5c9
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/main/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReader.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Standard implementation of Private Key Reader supporting PEM encoded PKCS8 
Private Keys using Java Cryptography Architecture components
+ */
+public class StandardPrivateKeyReader implements PrivateKeyReader {
+    static final String KEY_PASSWORD_REQUIRED = "Key Password required for 
encrypted Private Key";
+
+    private static final String PRIVATE_KEY_TYPE = "PRIVATE KEY";
+
+    private static final String ENCRYPTED_PRIVATE_KEY_TYPE = "ENCRYPTED 
PRIVATE KEY";
+
+    private static final String BEGIN_BOUNDARY_PREFIX = "-----BEGIN ";
+
+    private static final String BOUNDARY_SUFFIX = "-----";
+
+    private static final String END_BOUNDARY_FORMAT = "-----END %s-----";
+
+    private static final char[] EMPTY_PASSWORD = new char[0];
+
+    private static final Map<String, String> KEY_ALGORITHMS = Map.of(
+            "1.2.840.113549.1.1.1", "RSA",
+            "1.2.840.113549.1.1.10", "RSASSA-PSS",
+            "1.2.840.10045.2.1", "EC",
+            "1.2.840.10040.4.1", "DSA",
+            "1.3.101.110", "X25519",
+            "1.3.101.111", "X448",
+            "1.3.101.112", "Ed25519",
+            "1.3.101.113", "Ed448"
+    );
+
+    private static final Base64.Decoder DECODER = Base64.getDecoder();
+
+    private static final EncryptedPrivateKeyDecryptor 
ENCRYPTED_PRIVATE_KEY_DECRYPTOR = new EncryptedPrivateKeyDecryptor();
+
+    /**
+     * Read Private Key from the first PEM Private Key object in the stream 
with optional password for encrypted keys
+     *
+     * @param inputStream Key stream
+     * @param keyPassword Password for encrypted keys or empty when not 
encrypted
+     * @return Private Key
+     */
+    @Override
+    public PrivateKey readPrivateKey(final InputStream inputStream, final 
char[] keyPassword) {
+        Objects.requireNonNull(inputStream, "Input Stream required");
+        final char[] password = keyPassword == null ? EMPTY_PASSWORD : 
keyPassword;
+
+        try (final BufferedReader reader = new BufferedReader(new 
InputStreamReader(inputStream, StandardCharsets.US_ASCII))) {
+            final String type = readPrivateKeyType(reader);
+            return switch (type) {
+                case PRIVATE_KEY_TYPE -> getPrivateKey(readContent(reader, 
type));
+                case ENCRYPTED_PRIVATE_KEY_TYPE -> 
getEncryptedPrivateKey(readContent(reader, type), password);
+                default -> throw new IllegalArgumentException("Private Key 
[%s] not supported".formatted(type));
+            };
+        } catch (final IOException e) {
+            throw new UncheckedIOException("Read Private Key stream failed", 
e);
+        }
+    }
+
+    private String readPrivateKeyType(final BufferedReader reader) throws 
IOException {
+        String line = reader.readLine();
+        while (line != null) {
+            final String boundary = line.trim();
+            if (boundary.startsWith(BEGIN_BOUNDARY_PREFIX) && 
boundary.endsWith(BOUNDARY_SUFFIX)) {
+                final String type = 
boundary.substring(BEGIN_BOUNDARY_PREFIX.length(), boundary.length() - 
BOUNDARY_SUFFIX.length());
+                if (type.endsWith(PRIVATE_KEY_TYPE)) {
+                    return type;
+                }
+            }
+
+            line = reader.readLine();
+        }
+
+        throw new PrivateKeyException("PEM Private Key not found");
+    }
+
+    private byte[] readContent(final BufferedReader reader, final String type) 
throws IOException {
+        final String endBoundary = END_BOUNDARY_FORMAT.formatted(type);
+        final StringBuilder encoded = new StringBuilder();
+
+        String line = reader.readLine();
+        while (line != null) {
+            final String trimmedLine = line.trim();
+            if (endBoundary.equals(trimmedLine)) {
+                return decode(type, encoded.toString());
+            }
+
+            encoded.append(trimmedLine);
+            line = reader.readLine();
+        }
+
+        throw new PrivateKeyException("PEM boundary [%s] not 
found".formatted(endBoundary));
+    }
+
+    private byte[] decode(final String type, final String encoded) {
+        try {
+            return DECODER.decode(encoded);
+        } catch (final IllegalArgumentException e) {
+            throw new PrivateKeyException("PEM [%s] Base64 decoding 
failed".formatted(type), e);
+        }
+    }
+
+    private PrivateKey getEncryptedPrivateKey(final byte[] 
encryptedPrivateKeyInfo, final char[] password) {
+        try {
+            final byte[] privateKeyInfo = 
ENCRYPTED_PRIVATE_KEY_DECRYPTOR.decrypt(encryptedPrivateKeyInfo, password);
+            return getPrivateKey(privateKeyInfo);
+        } catch (final PrivateKeyException e) {
+            if (password.length == 0) {
+                throw new PrivateKeyException(KEY_PASSWORD_REQUIRED, e);
+            }
+
+            throw e;
+        }
+    }
+
+    private PrivateKey getPrivateKey(final byte[] privateKeyInfo) {
+        final String algorithmIdentifier = 
DerElement.read(privateKeyInfo).getElement(1).getElement(0).getObjectIdentifier();
+        final String keyAlgorithm = 
KEY_ALGORITHMS.getOrDefault(algorithmIdentifier, algorithmIdentifier);
+        try {
+            final KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
+            return keyFactory.generatePrivate(new 
PKCS8EncodedKeySpec(privateKeyInfo));
+        } catch (final NoSuchAlgorithmException e) {
+            throw new PrivateKeyException("Private Key Algorithm [%s] not 
supported".formatted(keyAlgorithm), e);
+        } catch (final InvalidKeySpecException e) {
+            throw new PrivateKeyException("Private Key Algorithm [%s] parsing 
failed".formatted(keyAlgorithm), e);
+        }
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
index 948312c6384..9fcb5965fac 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/StandardPrivateKeyServiceTest.java
@@ -22,13 +22,6 @@ import org.apache.nifi.util.NoOpProcessor;
 import org.apache.nifi.util.PropertyMigrationResult;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
-import org.bouncycastle.asn1.ASN1ObjectIdentifier;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
-import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
-import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.OutputEncryptor;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -36,33 +29,51 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
 import java.io.IOException;
-import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
+import java.util.Base64;
 import java.util.Map;
 import java.util.UUID;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
 
-import static 
org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder.AES_256_CBC;
-import static 
org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder.DES3_CBC;
-import static 
org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder.PBE_SHA1_3DES;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 class StandardPrivateKeyServiceTest {
     private static final String SERVICE_ID = 
StandardPrivateKeyServiceTest.class.getSimpleName();
 
-    private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new 
BouncyCastleProvider();
-
     private static final String PATH_NOT_FOUND = "/path/not/found";
 
     private static final String KEY_NOT_VALID = "-----BEGIN KEY NOT 
VALID-----";
 
     private static final String RSA_ALGORITHM = "RSA";
 
-    private static final OutputEncryptor DISABLED_ENCRYPTOR = null;
+    private static final String PBES2_HMAC_SHA256_AES_256 = 
"PBEWithHmacSHA256AndAES_256";
+
+    private static final String PBES2_HMAC_SHA512_AES_128 = 
"PBEWithHmacSHA512AndAES_128";
+
+    private static final String PBE_SHA1_DESEDE = "PBEWithSHA1AndDESede";
+
+    private static final String PBES2_ALGORITHM = "PBES2";
+
+    private static final String PBES2_CIPHER_PREFIX = "PBEWithHmac";
+
+    private static final String PRIVATE_KEY_TYPE = "PRIVATE KEY";
+
+    private static final String ENCRYPTED_PRIVATE_KEY_TYPE = "ENCRYPTED 
PRIVATE KEY";
+
+    private static final String PEM_FORMAT = "-----BEGIN %s-----\n%s\n-----END 
%s-----\n";
+
+    private static final Base64.Encoder ENCODER = Base64.getMimeEncoder();
 
     private static PrivateKey generatedPrivateKey;
 
@@ -102,8 +113,8 @@ class StandardPrivateKeyServiceTest {
     }
 
     @Test
-    void testGetPrivateKeyEncodedKey() throws Exception {
-        final String encodedPrivateKey = 
getEncodedPrivateKey(generatedPrivateKey, DISABLED_ENCRYPTOR);
+    void testGetPrivateKeyEncodedKey() {
+        final String encodedPrivateKey = getPem(PRIVATE_KEY_TYPE, 
generatedPrivateKey.getEncoded());
 
         runner.setProperty(service, StandardPrivateKeyService.KEY, 
encodedPrivateKey);
         runner.enableControllerService(service);
@@ -116,8 +127,7 @@ class StandardPrivateKeyServiceTest {
     @MethodSource("encryptionAlgorithms")
     void testGetPrivateKeyEncryptedKey(final String encryptionAlgorithm) 
throws Exception {
         final String password = UUID.randomUUID().toString();
-        final OutputEncryptor outputEncryptor = 
getOutputEncryptor(encryptionAlgorithm, password);
-        final String encryptedPrivateKey = 
getEncodedPrivateKey(generatedPrivateKey, outputEncryptor);
+        final String encryptedPrivateKey = 
getEncryptedPrivateKey(generatedPrivateKey, encryptionAlgorithm, password);
         final Path keyPath = writeKey(encryptedPrivateKey);
 
         runner.setProperty(service, StandardPrivateKeyService.KEY_FILE, 
keyPath.toString());
@@ -148,9 +158,9 @@ class StandardPrivateKeyServiceTest {
 
     private static String[] encryptionAlgorithms() {
         return new String[] {
-                AES_256_CBC,
-                DES3_CBC,
-                PBE_SHA1_3DES
+                PBES2_HMAC_SHA256_AES_256,
+                PBES2_HMAC_SHA512_AES_128,
+                PBE_SHA1_DESEDE
         };
     }
 
@@ -162,19 +172,35 @@ class StandardPrivateKeyServiceTest {
         return keyPath;
     }
 
-    private String getEncodedPrivateKey(final PrivateKey privateKey, final 
OutputEncryptor outputEncryptor) throws Exception {
-        final StringWriter stringWriter = new StringWriter();
-        try (final JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
-            final JcaPKCS8Generator generator = new 
JcaPKCS8Generator(privateKey, outputEncryptor);
-            pemWriter.writeObject(generator);
+    private String getEncryptedPrivateKey(final PrivateKey privateKey, final 
String encryptionAlgorithm, final String password) throws 
GeneralSecurityException, IOException {
+        final SecretKey secretKey = 
SecretKeyFactory.getInstance(encryptionAlgorithm).generateSecret(new 
PBEKeySpec(password.toCharArray()));
+        final Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+        final byte[] encrypted = cipher.doFinal(privateKey.getEncoded());
+
+        final EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new 
EncryptedPrivateKeyInfo(getEncryptionParameters(cipher), encrypted);
+        return getPem(ENCRYPTED_PRIVATE_KEY_TYPE, 
encryptedPrivateKeyInfo.getEncoded());
+    }
+
+    /**
+     * Get encryption parameters with an algorithm name that 
EncryptedPrivateKeyInfo can resolve to an Object Identifier,
+     * which requires PBES2 instead of cipher names such as 
PBEWithHmacSHA256AndAES_256
+     */
+    private AlgorithmParameters getEncryptionParameters(final Cipher cipher) 
throws GeneralSecurityException, IOException {
+        final AlgorithmParameters cipherParameters = cipher.getParameters();
+
+        final AlgorithmParameters encryptionParameters;
+        if (cipherParameters.getAlgorithm().startsWith(PBES2_CIPHER_PREFIX)) {
+            encryptionParameters = 
AlgorithmParameters.getInstance(PBES2_ALGORITHM);
+            encryptionParameters.init(cipherParameters.getEncoded());
+        } else {
+            encryptionParameters = cipherParameters;
         }
-        return stringWriter.toString();
+
+        return encryptionParameters;
     }
 
-    private OutputEncryptor getOutputEncryptor(final String 
encryptionAlgorithm, final String password) throws OperatorCreationException {
-        return new JceOpenSSLPKCS8EncryptorBuilder(new 
ASN1ObjectIdentifier(encryptionAlgorithm))
-                .setProvider(BOUNCY_CASTLE_PROVIDER)
-                .setPassword(password.toCharArray())
-                .build();
+    private String getPem(final String type, final byte[] encoded) {
+        return PEM_FORMAT.formatted(type, ENCODER.encodeToString(encoded), 
type);
     }
 }
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/DerElementTest.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/DerElementTest.java
new file mode 100644
index 00000000000..e7e86452071
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/DerElementTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.HexFormat;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class DerElementTest {
+    private static final HexFormat HEX_FORMAT = HexFormat.of();
+
+    private static final int LONG_FORM_CONTENTS_LENGTH = 300;
+
+    private static final String LONG_FORM_OCTET_STRING_HEADER = "0482012c";
+
+    private static final String SEQUENCE_INTEGER_ONE = "3003020101";
+
+    private static final String INTEGER_EMPTY = "0200";
+
+    @Test
+    void testRead() {
+        final DerElement sequence = 
DerElement.read(HEX_FORMAT.parseHex(SEQUENCE_INTEGER_ONE));
+
+        assertEquals(DerElement.SEQUENCE_TAG, sequence.getTag());
+        assertEquals(1, sequence.getElements().size());
+        assertEquals(BigInteger.ONE, sequence.getElement(0).getInteger());
+    }
+
+    @Test
+    void testReadLongFormLength() {
+        final byte[] header = 
HEX_FORMAT.parseHex(LONG_FORM_OCTET_STRING_HEADER);
+        final byte[] encoded = Arrays.copyOf(header, header.length + 
LONG_FORM_CONTENTS_LENGTH);
+
+        final DerElement octetString = DerElement.read(encoded);
+
+        assertArrayEquals(new byte[LONG_FORM_CONTENTS_LENGTH], 
octetString.getOctetString());
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "06092a864886f70d01050d,1.2.840.113549.1.5.13",
+            "06092b06010401da47040b,1.3.6.1.4.1.11591.4.11",
+            "06072a8648ce3d0201,1.2.840.10045.2.1",
+            "0603883703,2.999.3"
+    })
+    void testGetObjectIdentifier(final String encoded, final String 
expectedObjectIdentifier) {
+        final DerElement element = 
DerElement.read(HEX_FORMAT.parseHex(encoded));
+
+        assertEquals(expectedObjectIdentifier, element.getObjectIdentifier());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "0600",
+            "06022a86",
+            "06032a8001",
+            "060b2affffffffffffffffff7f"
+    })
+    void testGetObjectIdentifierException(final String encoded) {
+        final DerElement element = 
DerElement.read(HEX_FORMAT.parseHex(encoded));
+
+        assertThrows(PrivateKeyException.class, element::getObjectIdentifier);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "",
+            "30",
+            "3080",
+            "3082",
+            "30850100000000",
+            "3005020100",
+            "1f0100",
+            "30030201000500"
+    })
+    void testReadException(final String encoded) {
+        final byte[] bytes = HEX_FORMAT.parseHex(encoded);
+
+        assertThrows(PrivateKeyException.class, () -> DerElement.read(bytes));
+    }
+
+    @Test
+    void testGetElementException() {
+        final DerElement sequence = 
DerElement.read(HEX_FORMAT.parseHex(SEQUENCE_INTEGER_ONE));
+
+        assertThrows(PrivateKeyException.class, () -> sequence.getElement(1));
+        assertThrows(PrivateKeyException.class, sequence::getInteger);
+        assertThrows(PrivateKeyException.class, sequence::getOctetString);
+
+        final DerElement integer = sequence.getElement(0);
+        assertThrows(PrivateKeyException.class, integer::getElements);
+        assertThrows(PrivateKeyException.class, integer::getObjectIdentifier);
+    }
+
+    @Test
+    void testGetIntegerException() {
+        final DerElement integer = 
DerElement.read(HEX_FORMAT.parseHex(INTEGER_EMPTY));
+
+        assertThrows(PrivateKeyException.class, integer::getInteger);
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReaderTest.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReaderTest.java
new file mode 100644
index 00000000000..ee3c6491f11
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-key-service-bundle/nifi-key-service/src/test/java/org/apache/nifi/key/service/reader/StandardPrivateKeyReaderTest.java
@@ -0,0 +1,317 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.key.service.reader;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.spec.ECGenParameterSpec;
+import java.util.Base64;
+import java.util.HexFormat;
+import java.util.UUID;
+import java.util.stream.Stream;
+import javax.crypto.Cipher;
+import javax.crypto.EncryptedPrivateKeyInfo;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StandardPrivateKeyReaderTest {
+    private static final HexFormat HEX_FORMAT = HexFormat.of();
+
+    private static final Base64.Encoder ENCODER = Base64.getEncoder();
+
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+    private static final char[] KEY_PASSWORD = 
UUID.randomUUID().toString().toCharArray();
+
+    private static final char[] INCORRECT_KEY_PASSWORD = 
UUID.randomUUID().toString().toCharArray();
+
+    private static final char[] EMPTY_KEY_PASSWORD = new char[0];
+
+    private static final char[] NULL_KEY_PASSWORD = null;
+
+    private static final String PBES2_HMAC_SHA1_AES_128 = 
"PBEWithHmacSHA1AndAES_128";
+
+    private static final String PBES2_HMAC_SHA224_AES_256 = 
"PBEWithHmacSHA224AndAES_256";
+
+    private static final String PBES2_HMAC_SHA256_AES_256 = 
"PBEWithHmacSHA256AndAES_256";
+
+    private static final String PBES2_HMAC_SHA384_AES_128 = 
"PBEWithHmacSHA384AndAES_128";
+
+    private static final String PBES2_HMAC_SHA512_AES_256 = 
"PBEWithHmacSHA512AndAES_256";
+
+    private static final String PBES2_HMAC_SHA512_224_AES_128 = 
"PBEWithHmacSHA512/224AndAES_128";
+
+    private static final String PBES2_HMAC_SHA512_256_AES_256 = 
"PBEWithHmacSHA512/256AndAES_256";
+
+    private static final String PBE_SHA1_DESEDE = "PBEWithSHA1AndDESede";
+
+    private static final String PBES2_ALGORITHM = "PBES2";
+
+    private static final String PBES2_CIPHER_PREFIX = "PBEWithHmac";
+
+    private static final String PBKDF2_HMAC_SHA256 = "PBKDF2WithHmacSHA256";
+
+    private static final int PBKDF2_ITERATION_COUNT = 2048;
+
+    private static final int SALT_LENGTH = 16;
+
+    private static final String DES_EDE3_ALGORITHM = "DESede";
+
+    private static final String DES_EDE3_CBC_TRANSFORMATION = 
"DESede/CBC/PKCS5Padding";
+
+    private static final int DES_EDE3_KEY_SIZE = 192;
+
+    private static final byte[] PBES2_OBJECT_IDENTIFIER = 
HEX_FORMAT.parseHex("06092a864886f70d01050d");
+
+    private static final byte[] PBKDF2_OBJECT_IDENTIFIER = 
HEX_FORMAT.parseHex("06092a864886f70d01050c");
+
+    private static final byte[] HMAC_SHA256_OBJECT_IDENTIFIER = 
HEX_FORMAT.parseHex("06082a864886f70d0209");
+
+    private static final byte[] DES_EDE3_CBC_OBJECT_IDENTIFIER = 
HEX_FORMAT.parseHex("06082a864886f70d0307");
+
+    private static final byte[] NULL_PARAMETERS = HEX_FORMAT.parseHex("0500");
+
+    private static final int DER_LONG_FORM_LENGTH_FLAG = 0x80;
+
+    private static final String RSA_ALGORITHM = "RSA";
+
+    private static final int RSA_KEY_SIZE = 2048;
+
+    private static final String EC_ALGORITHM = "EC";
+
+    private static final String ED25519_ALGORITHM = "Ed25519";
+
+    private static final String CURVE_P_256 = "secp256r1";
+
+    private static final String CURVE_P_384 = "secp384r1";
+
+    private static final String PRIVATE_KEY_TYPE = "PRIVATE KEY";
+
+    private static final String ENCRYPTED_PRIVATE_KEY_TYPE = "ENCRYPTED 
PRIVATE KEY";
+
+    private static final String CERTIFICATE_TYPE = "CERTIFICATE";
+
+    private static final String PEM_FORMAT = "-----BEGIN %s-----\n%s\n-----END 
%s-----\n";
+
+    private static final String BAG_ATTRIBUTES = "Bag Attributes\n    
localKeyID: 01 00 00 00\nKey Attributes: <No Attributes>\n";
+
+    private static final String BASE64_NOT_VALID = "-----BEGIN PRIVATE 
KEY-----\n!!!!\n-----END PRIVATE KEY-----\n";
+
+    private static final String END_BOUNDARY_NOT_FOUND = "-----BEGIN PRIVATE 
KEY-----\nMAA=\n";
+
+    private static final byte[] CONTENT = {1};
+
+    private static final byte[] DER_NOT_VALID = {0x30};
+
+    private static PrivateKey rsaPrivateKey;
+
+    private static PrivateKey ecCurveP256PrivateKey;
+
+    private static PrivateKey ecCurveP384PrivateKey;
+
+    private static PrivateKey ed25519PrivateKey;
+
+    private final StandardPrivateKeyReader reader = new 
StandardPrivateKeyReader();
+
+    @BeforeAll
+    static void setPrivateKeys() throws GeneralSecurityException {
+        final KeyPairGenerator rsaKeyPairGenerator = 
KeyPairGenerator.getInstance(RSA_ALGORITHM);
+        rsaKeyPairGenerator.initialize(RSA_KEY_SIZE);
+        rsaPrivateKey = rsaKeyPairGenerator.generateKeyPair().getPrivate();
+
+        ecCurveP256PrivateKey = getEcPrivateKey(CURVE_P_256);
+        ecCurveP384PrivateKey = getEcPrivateKey(CURVE_P_384);
+        ed25519PrivateKey = 
KeyPairGenerator.getInstance(ED25519_ALGORITHM).generateKeyPair().getPrivate();
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("privateKeyArguments")
+    void testReadPrivateKey(final String description, final String pem, final 
char[] keyPassword, final PrivateKey expectedPrivateKey) {
+        final PrivateKey privateKey = readPrivateKey(pem, keyPassword);
+
+        assertEquals(expectedPrivateKey, privateKey);
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("privateKeyPasswordRequiredArguments")
+    void testReadPrivateKeyPasswordRequired(final String description, final 
String pem) {
+        final PrivateKeyException exception = 
assertThrows(PrivateKeyException.class, () -> readPrivateKey(pem, 
EMPTY_KEY_PASSWORD));
+
+        assertEquals(StandardPrivateKeyReader.KEY_PASSWORD_REQUIRED, 
exception.getMessage());
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("privateKeyExceptionArguments")
+    void testReadPrivateKeyException(final String description, final String 
pem, final char[] keyPassword) {
+        assertThrows(PrivateKeyException.class, () -> readPrivateKey(pem, 
keyPassword));
+    }
+
+    static Stream<Arguments> privateKeyArguments() throws 
GeneralSecurityException, IOException {
+        return Stream.of(
+                Arguments.of("RSA", getPkcs8Pem(rsaPrivateKey), 
EMPTY_KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("RSA null password", getPkcs8Pem(rsaPrivateKey), 
NULL_KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("EC P-256", getPkcs8Pem(ecCurveP256PrivateKey), 
EMPTY_KEY_PASSWORD, ecCurveP256PrivateKey),
+                Arguments.of("Ed25519", getPkcs8Pem(ed25519PrivateKey), 
EMPTY_KEY_PASSWORD, ed25519PrivateKey),
+                Arguments.of("PBES2 HMAC-SHA1 AES-128-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA1_AES_128), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA224 AES-256-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA224_AES_256), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA256 AES-256-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA256_AES_256), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA384 AES-128-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA384_AES_128), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA512 AES-256-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA512_AES_256), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA512/224 AES-128-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA512_224_AES_128), 
KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA512/256 AES-256-CBC RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA512_256_AES_256), 
KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA256 DES-EDE3-CBC RSA", 
getPbes2DesEde3Pem(rsaPrivateKey), KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("PBES2 HMAC-SHA256 AES-256-CBC EC P-384", 
getEncryptedPkcs8Pem(ecCurveP384PrivateKey, PBES2_HMAC_SHA256_AES_256), 
KEY_PASSWORD, ecCurveP384PrivateKey),
+                Arguments.of("PBES2 HMAC-SHA256 AES-256-CBC Ed25519", 
getEncryptedPkcs8Pem(ed25519PrivateKey, PBES2_HMAC_SHA256_AES_256), 
KEY_PASSWORD, ed25519PrivateKey),
+                Arguments.of("PBE-SHA1-3DES RSA", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBE_SHA1_DESEDE), KEY_PASSWORD, 
rsaPrivateKey),
+                Arguments.of("RSA with preceding Bag Attributes", 
BAG_ATTRIBUTES + getPkcs8Pem(rsaPrivateKey), EMPTY_KEY_PASSWORD, rsaPrivateKey),
+                Arguments.of("RSA with preceding Certificate", 
getPem(CERTIFICATE_TYPE, CONTENT) + getPkcs8Pem(rsaPrivateKey), 
EMPTY_KEY_PASSWORD, rsaPrivateKey)
+        );
+    }
+
+    static Stream<Arguments> privateKeyPasswordRequiredArguments() throws 
GeneralSecurityException, IOException {
+        return Stream.of(
+                Arguments.of("PBES2 HMAC-SHA256 AES-256-CBC", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA256_AES_256)),
+                Arguments.of("PBE-SHA1-3DES", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBE_SHA1_DESEDE))
+        );
+    }
+
+    static Stream<Arguments> privateKeyExceptionArguments() throws 
GeneralSecurityException, IOException {
+        return Stream.of(
+                Arguments.of("PBES2 HMAC-SHA256 AES-256-CBC incorrect 
password", getEncryptedPkcs8Pem(rsaPrivateKey, PBES2_HMAC_SHA256_AES_256), 
INCORRECT_KEY_PASSWORD),
+                Arguments.of("PBE-SHA1-3DES incorrect password", 
getEncryptedPkcs8Pem(rsaPrivateKey, PBE_SHA1_DESEDE), INCORRECT_KEY_PASSWORD),
+                Arguments.of("DER not valid", getPem(PRIVATE_KEY_TYPE, 
DER_NOT_VALID), EMPTY_KEY_PASSWORD),
+                Arguments.of("PEM Base64 not valid", BASE64_NOT_VALID, 
EMPTY_KEY_PASSWORD),
+                Arguments.of("PEM end boundary not found", 
END_BOUNDARY_NOT_FOUND, EMPTY_KEY_PASSWORD),
+                Arguments.of("PEM Private Key not found", 
getPem(CERTIFICATE_TYPE, CONTENT), EMPTY_KEY_PASSWORD)
+        );
+    }
+
+    private PrivateKey readPrivateKey(final String pem, final char[] 
keyPassword) {
+        return reader.readPrivateKey(new 
ByteArrayInputStream(pem.getBytes(StandardCharsets.US_ASCII)), keyPassword);
+    }
+
+    private static PrivateKey getEcPrivateKey(final String curveName) throws 
GeneralSecurityException {
+        final KeyPairGenerator keyPairGenerator = 
KeyPairGenerator.getInstance(EC_ALGORITHM);
+        keyPairGenerator.initialize(new ECGenParameterSpec(curveName));
+        return keyPairGenerator.generateKeyPair().getPrivate();
+    }
+
+    private static String getPem(final String type, final byte[] content) {
+        return PEM_FORMAT.formatted(type, ENCODER.encodeToString(content), 
type);
+    }
+
+    private static String getPkcs8Pem(final PrivateKey privateKey) {
+        return getPem(PRIVATE_KEY_TYPE, privateKey.getEncoded());
+    }
+
+    private static String getEncryptedPkcs8Pem(final PrivateKey privateKey, 
final String encryptionAlgorithm) throws GeneralSecurityException, IOException {
+        final SecretKey secretKey = 
SecretKeyFactory.getInstance(encryptionAlgorithm).generateSecret(new 
PBEKeySpec(KEY_PASSWORD));
+        final Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
+        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+        final byte[] encrypted = cipher.doFinal(privateKey.getEncoded());
+
+        final EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new 
EncryptedPrivateKeyInfo(getEncryptionParameters(cipher), encrypted);
+        return getPem(ENCRYPTED_PRIVATE_KEY_TYPE, 
encryptedPrivateKeyInfo.getEncoded());
+    }
+
+    private static AlgorithmParameters getEncryptionParameters(final Cipher 
cipher) throws GeneralSecurityException, IOException {
+        final AlgorithmParameters cipherParameters = cipher.getParameters();
+
+        final AlgorithmParameters encryptionParameters;
+        if (cipherParameters.getAlgorithm().startsWith(PBES2_CIPHER_PREFIX)) {
+            encryptionParameters = 
AlgorithmParameters.getInstance(PBES2_ALGORITHM);
+            encryptionParameters.init(cipherParameters.getEncoded());
+        } else {
+            encryptionParameters = cipherParameters;
+        }
+
+        return encryptionParameters;
+    }
+
+    private static String getPbes2DesEde3Pem(final PrivateKey privateKey) 
throws GeneralSecurityException {
+        final byte[] salt = new byte[SALT_LENGTH];
+        SECURE_RANDOM.nextBytes(salt);
+        final PBEKeySpec keySpec = new PBEKeySpec(KEY_PASSWORD, salt, 
PBKDF2_ITERATION_COUNT, DES_EDE3_KEY_SIZE);
+        final byte[] key = 
SecretKeyFactory.getInstance(PBKDF2_HMAC_SHA256).generateSecret(keySpec).getEncoded();
+
+        final Cipher cipher = Cipher.getInstance(DES_EDE3_CBC_TRANSFORMATION);
+        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, 
DES_EDE3_ALGORITHM));
+        final byte[] encrypted = cipher.doFinal(privateKey.getEncoded());
+
+        final byte[] pseudoRandomFunction = 
getSequence(HMAC_SHA256_OBJECT_IDENTIFIER, NULL_PARAMETERS);
+        final byte[] pbkdf2Parameters = getSequence(getOctetString(salt), 
getInteger(), pseudoRandomFunction);
+        final byte[] keyDerivationFunction = 
getSequence(PBKDF2_OBJECT_IDENTIFIER, pbkdf2Parameters);
+        final byte[] encryptionScheme = 
getSequence(DES_EDE3_CBC_OBJECT_IDENTIFIER, getOctetString(cipher.getIV()));
+        final byte[] encryptionAlgorithm = 
getSequence(PBES2_OBJECT_IDENTIFIER, getSequence(keyDerivationFunction, 
encryptionScheme));
+        final byte[] encryptedPrivateKeyInfo = 
getSequence(encryptionAlgorithm, getOctetString(encrypted));
+        return getPem(ENCRYPTED_PRIVATE_KEY_TYPE, encryptedPrivateKeyInfo);
+    }
+
+    private static byte[] getSequence(final byte[]... elements) {
+        return getDerEncoded(DerElement.SEQUENCE_TAG, elements);
+    }
+
+    private static byte[] getOctetString(final byte[] contents) {
+        return getDerEncoded(DerElement.OCTET_STRING_TAG, contents);
+    }
+
+    private static byte[] getInteger() {
+        return getDerEncoded(DerElement.INTEGER_TAG, 
BigInteger.valueOf(PBKDF2_ITERATION_COUNT).toByteArray());
+    }
+
+    private static byte[] getDerEncoded(final int tag, final byte[]... 
contents) {
+        final ByteArrayOutputStream contentsStream = new 
ByteArrayOutputStream();
+        for (final byte[] content : contents) {
+            contentsStream.writeBytes(content);
+        }
+
+        final ByteArrayOutputStream encodedStream = new 
ByteArrayOutputStream();
+        encodedStream.write(tag);
+
+        final int length = contentsStream.size();
+        if (length < DER_LONG_FORM_LENGTH_FLAG) {
+            encodedStream.write(length);
+        } else {
+            final byte[] signedLength = 
BigInteger.valueOf(length).toByteArray();
+            final int lengthOffset = signedLength[0] == 0 ? 1 : 0;
+            final int lengthBytes = signedLength.length - lengthOffset;
+            encodedStream.write(DER_LONG_FORM_LENGTH_FLAG | lengthBytes);
+            encodedStream.write(signedLength, lengthOffset, lengthBytes);
+        }
+
+        encodedStream.writeBytes(contentsStream.toByteArray());
+        return encodedStream.toByteArray();
+    }
+}

Reply via email to