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();
+ }
+}