2121import javax .crypto .SecretKeyFactory ;
2222import javax .crypto .spec .PBEKeySpec ;
2323import java .io .ByteArrayInputStream ;
24- import java .io .File ;
2524import java .io .IOException ;
26- import java .nio .file .Files ;
27- import java .security .*;
25+ import java .security .GeneralSecurityException ;
26+ import java .security .KeyFactory ;
27+ import java .security .KeyStore ;
28+ import java .security .KeyStoreException ;
29+ import java .security .PrivateKey ;
2830import java .security .cert .Certificate ;
2931import java .security .cert .CertificateException ;
3032import java .security .cert .CertificateFactory ;
@@ -57,26 +59,47 @@ public final class PemReader {
5759
5860 private static final Pattern CERT_PATTERN = Pattern .compile (
5961 "-+BEGIN\\ s+.*CERTIFICATE[^-]*-+\\ s*" // Header
60- + "([a-z0-9+/=\\ r \\ n ]+)" // Base64 text
62+ + "([a-z0-9+/=\\ s ]+)" // Base64 text
6163 + "-+END\\ s+.*CERTIFICATE[^-]*-+" , // Footer
6264 CASE_INSENSITIVE );
6365
6466 private static final Pattern PRIVATE_KEY_PATTERN = Pattern .compile (
6567 "-+BEGIN\\ s+.*PRIVATE\\ s+KEY[^-]*-+\\ s*" // Header
66- + "([a-z0-9+/=\\ r \\ n ]+)" // Base64 text
68+ + "([a-z0-9+/=\\ s ]+)" // Base64 text
6769 + "-+END\\ s+.*PRIVATE\\ s+KEY[^-]*-+" , // Footer
6870 CASE_INSENSITIVE );
6971
7072 private PemReader () {
7173 }
7274
73- public static KeyStore loadKeyStore (String certificateChainFile , String privateKeyFile , Optional <String > keyPassword ) throws IOException , GeneralSecurityException {
74- PrivateKey key = loadPrivateKey (privateKeyFile , keyPassword );
75-
76- List <X509Certificate > certificateChain = readCertificateChain (certificateChainFile );
75+ /**
76+ * Loads a KeyStore from PEM file contents.
77+ * <p>
78+ * This method reads a private key and certificate chain from PEM-formatted content
79+ * and stores them in a JKS KeyStore. The certificate file must contain at least one
80+ * certificate.
81+ *
82+ * @param certificateChainContents the PEM-formatted content containing the certificate chain
83+ * @param privateKeyContents the PEM-formatted content containing the private key
84+ * @param keyPassword optional password for the private key; if the key is encrypted, this password
85+ * will be used to decrypt it
86+ * @return a KeyStore containing the private key and certificate chain
87+ * @throws IOException if an I/O error occurs while reading the PEM content
88+ * @throws GeneralSecurityException if a security-related error occurs, such as:
89+ * <ul>
90+ * <li>the private key cannot be loaded</li>
91+ * <li>the certificate chain is empty</li>
92+ * <li>the KeyStore cannot be initialized</li>
93+ * </ul>
94+ * @throws CertificateException if the certificate file does not contain any certificates
95+ */
96+ public static KeyStore loadKeyStore (String certificateChainContents , String privateKeyContents , Optional <String > keyPassword ) throws IOException , GeneralSecurityException {
97+ PrivateKey key = loadPrivateKey (privateKeyContents , keyPassword );
98+
99+ List <X509Certificate > certificateChain = readCertificateChain (certificateChainContents );
77100 if (certificateChain .isEmpty ()) {
78101 throw new CertificateException ("Certificate file does not contain any certificates: "
79- + certificateChainFile );
102+ + certificateChainContents );
80103 }
81104
82105 KeyStore keyStore = KeyStore .getInstance ("JKS" );
@@ -88,8 +111,20 @@ public static KeyStore loadKeyStore(String certificateChainFile, String privateK
88111 return keyStore ;
89112 }
90113
91- public static List <X509Certificate > readCertificateChain (String certificateChain ) throws CertificateException {
92- Matcher matcher = CERT_PATTERN .matcher (certificateChain );
114+ /**
115+ * Reads a chain of X.509 certificates from PEM-formatted content.
116+ * <p>
117+ * This method extracts all certificates found in the provided PEM content. Certificates
118+ * are identified by BEGIN CERTIFICATE and END CERTIFICATE markers. The certificates are
119+ * returned in the order they appear in the input.
120+ *
121+ * @param certificateChainContents the PEM-formatted content containing one or more certificates
122+ * @return a list of X.509 certificates extracted from the PEM content; may be empty if no
123+ * certificates are found
124+ * @throws CertificateException if any certificate cannot be parsed or generated
125+ */
126+ public static List <X509Certificate > readCertificateChain (String certificateChainContents ) throws CertificateException {
127+ Matcher matcher = CERT_PATTERN .matcher (certificateChainContents );
93128 CertificateFactory certificateFactory = CertificateFactory .getInstance ("X.509" );
94129 List <X509Certificate > certificates = new ArrayList <>();
95130
@@ -103,6 +138,28 @@ public static List<X509Certificate> readCertificateChain(String certificateChain
103138 return certificates ;
104139 }
105140
141+ /**
142+ * Loads a private key from PEM-formatted content.
143+ * <p>
144+ * This method supports both encrypted and unencrypted private keys. The key must be in
145+ * PKCS#8 format. To convert a key to PKCS#8 format, use:
146+ * {@code openssl pkcs8 -topk8 ...}
147+ * <p>
148+ * The method attempts to load the key using RSA, EC, and DSA algorithms in that order.
149+ *
150+ * @param privateKey the PEM-formatted content containing the private key
151+ * @param keyPassword optional password for decrypting an encrypted private key; if empty,
152+ * the key is assumed to be unencrypted
153+ * @return the loaded private key
154+ * @throws IOException if an I/O error occurs while reading the key
155+ * @throws GeneralSecurityException if a security-related error occurs, such as:
156+ * <ul>
157+ * <li>no private key is found in the content</li>
158+ * <li>the key format is invalid</li>
159+ * <li>the key cannot be decrypted with the provided password</li>
160+ * <li>the key algorithm is not supported (RSA, EC, or DSA)</li>
161+ * </ul>
162+ */
106163 public static PrivateKey loadPrivateKey (String privateKey , Optional <String > keyPassword ) throws IOException , GeneralSecurityException {
107164 Matcher matcher = PRIVATE_KEY_PATTERN .matcher (privateKey );
108165 if (!matcher .find ()) {
@@ -126,20 +183,28 @@ public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyP
126183
127184 // this code requires a key in PKCS8 format which is not the default openssl format
128185 // to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
186+ List <String > attemptedAlgorithms = new ArrayList <>();
129187 try {
130188 KeyFactory keyFactory = KeyFactory .getInstance ("RSA" );
131189 return keyFactory .generatePrivate (encodedKeySpec );
132- } catch (InvalidKeySpecException ignore ) {
190+ } catch (InvalidKeySpecException e ) {
191+ attemptedAlgorithms .add ("RSA: " + e .getMessage ());
133192 }
134193
135194 try {
136195 KeyFactory keyFactory = KeyFactory .getInstance ("EC" );
137196 return keyFactory .generatePrivate (encodedKeySpec );
138- } catch (InvalidKeySpecException ignore ) {
197+ } catch (InvalidKeySpecException e ) {
198+ attemptedAlgorithms .add ("RSA: " + e .getMessage ());
139199 }
140200
141- KeyFactory keyFactory = KeyFactory .getInstance ("DSA" );
142- return keyFactory .generatePrivate (encodedKeySpec );
201+ try {
202+ return KeyFactory .getInstance ("DSA" ).generatePrivate (encodedKeySpec );
203+ } catch (InvalidKeySpecException e ) {
204+ attemptedAlgorithms .add ("DSA: " + e .getMessage ());
205+ throw new KeyStoreException (
206+ "Failed to load private key with any supported algorithm. Attempts: " + attemptedAlgorithms , e );
207+ }
143208 }
144209
145210 private static byte [] base64Decode (String base64 ) {
0 commit comments