View Javadoc
1   /*
2    * Copyright 2022 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.ssl;
17  
18  import io.netty.util.CharsetUtil;
19  import io.netty.util.internal.logging.InternalLogger;
20  import io.netty.util.internal.logging.InternalLoggerFactory;
21  import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
22  import org.bouncycastle.openssl.PEMDecryptorProvider;
23  import org.bouncycastle.openssl.PEMEncryptedKeyPair;
24  import org.bouncycastle.openssl.PEMKeyPair;
25  import org.bouncycastle.openssl.PEMParser;
26  import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
27  import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
28  import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
29  import org.bouncycastle.operator.InputDecryptorProvider;
30  import org.bouncycastle.operator.OperatorCreationException;
31  import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
32  import org.bouncycastle.pkcs.PKCSException;
33  
34  import java.io.File;
35  import java.io.FileNotFoundException;
36  import java.io.FileReader;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.InputStreamReader;
40  import java.security.AccessController;
41  import java.security.PrivateKey;
42  import java.security.PrivilegedAction;
43  import java.security.Provider;
44  
45  final class BouncyCastlePemReader {
46      private static final String BC_PROVIDER = "org.bouncycastle.jce.provider.BouncyCastleProvider";
47      private static final String BC_PEMPARSER = "org.bouncycastle.openssl.PEMParser";
48      private static final InternalLogger logger = InternalLoggerFactory.getInstance(BouncyCastlePemReader.class);
49  
50      private static volatile Throwable unavailabilityCause;
51      private static volatile Provider bcProvider;
52      private static volatile boolean attemptedLoading;
53  
54      public static boolean hasAttemptedLoading() {
55          return attemptedLoading;
56      }
57  
58      public static boolean isAvailable() {
59          if (!hasAttemptedLoading()) {
60              tryLoading();
61          }
62          return unavailabilityCause == null;
63      }
64  
65      /**
66       * @return the cause if unavailable. {@code null} if available.
67       */
68      public static Throwable unavailabilityCause() {
69          return unavailabilityCause;
70      }
71  
72      private static void tryLoading() {
73          AccessController.doPrivileged(new PrivilegedAction<Void>() {
74              @Override
75              public Void run() {
76                  try {
77                      ClassLoader classLoader = getClass().getClassLoader();
78                      // Check for bcprov-jdk15on:
79                      Class<Provider> bcProviderClass =
80                              (Class<Provider>) Class.forName(BC_PROVIDER, true, classLoader);
81                      // Check for bcpkix-jdk15on:
82                      Class.forName(BC_PEMPARSER, true, classLoader);
83                      bcProvider = bcProviderClass.getConstructor().newInstance();
84                      logger.debug("Bouncy Castle provider available");
85                      attemptedLoading = true;
86                  } catch (Throwable e) {
87                      logger.debug("Cannot load Bouncy Castle provider", e);
88                      unavailabilityCause = e;
89                      attemptedLoading = true;
90                  }
91                  return null;
92              }
93          });
94      }
95  
96      /**
97       * Generates a new {@link PrivateKey}.
98       *
99       * @param keyInputStream an input stream for a PKCS#1 or PKCS#8 private key in PEM format.
100      * @param keyPassword the password of the {@code keyFile}.
101      *                    {@code null} if it's not password-protected.
102      * @return generated {@link PrivateKey}.
103      */
104     public static PrivateKey getPrivateKey(InputStream keyInputStream, String keyPassword) {
105         if (!isAvailable()) {
106             if (logger.isDebugEnabled()) {
107                 logger.debug("Bouncy castle provider is unavailable.", unavailabilityCause());
108             }
109             return null;
110         }
111         try {
112             PEMParser parser = newParser(keyInputStream);
113             return getPrivateKey(parser, keyPassword);
114         } catch (Exception e) {
115             logger.debug("Unable to extract private key", e);
116             return null;
117         }
118     }
119 
120     /**
121      * Generates a new {@link PrivateKey}.
122      *
123      * @param keyFile a PKCS#1 or PKCS#8 private key file in PEM format.
124      * @param keyPassword the password of the {@code keyFile}.
125      *                    {@code null} if it's not password-protected.
126      * @return generated {@link PrivateKey}.
127      */
128     public static PrivateKey getPrivateKey(File keyFile, String keyPassword) {
129         if (!isAvailable()) {
130             if (logger.isDebugEnabled()) {
131                 logger.debug("Bouncy castle provider is unavailable.", unavailabilityCause());
132             }
133             return null;
134         }
135         try {
136             PEMParser parser = newParser(keyFile);
137             return getPrivateKey(parser, keyPassword);
138         } catch (Exception e) {
139             logger.debug("Unable to extract private key", e);
140             return null;
141         }
142     }
143 
144     private static JcaPEMKeyConverter newConverter() {
145         return new JcaPEMKeyConverter().setProvider(bcProvider);
146     }
147 
148     private static PrivateKey getPrivateKey(PEMParser pemParser, String keyPassword) throws IOException,
149             PKCSException, OperatorCreationException {
150         try {
151             JcaPEMKeyConverter converter = newConverter();
152             PrivateKey pk = null;
153 
154             Object object = pemParser.readObject();
155             while (object != null && pk == null) {
156                 if (logger.isDebugEnabled()) {
157                     logger.debug("Parsed PEM object of type {} and assume " +
158                                  "key is {}encrypted", object.getClass().getName(), keyPassword == null? "not " : "");
159                 }
160 
161                 if (keyPassword == null) {
162                     // assume private key is not encrypted
163                     if (object instanceof PrivateKeyInfo) {
164                         pk = converter.getPrivateKey((PrivateKeyInfo) object);
165                     } else if (object instanceof PEMKeyPair) {
166                         pk = converter.getKeyPair((PEMKeyPair) object).getPrivate();
167                     } else {
168                         logger.debug("Unable to handle PEM object of type {} as a non encrypted key",
169                                      object.getClass());
170                     }
171                 } else {
172                     // assume private key is encrypted
173                     if (object instanceof PEMEncryptedKeyPair) {
174                         PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
175                                 .setProvider(bcProvider)
176                                 .build(keyPassword.toCharArray());
177                         pk = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate();
178                     } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
179                         InputDecryptorProvider pkcs8InputDecryptorProvider =
180                                 new JceOpenSSLPKCS8DecryptorProviderBuilder()
181                                         .setProvider(bcProvider)
182                                         .build(keyPassword.toCharArray());
183                         pk = converter.getPrivateKey(((PKCS8EncryptedPrivateKeyInfo) object)
184                                                              .decryptPrivateKeyInfo(pkcs8InputDecryptorProvider));
185                     } else {
186                         logger.debug("Unable to handle PEM object of type {} as a encrypted key", object.getClass());
187                     }
188                 }
189 
190                 // Try reading next entry in the pem file if private key is not yet found
191                 if (pk == null) {
192                     object = pemParser.readObject();
193                 }
194             }
195 
196             if (pk == null) {
197                 if (logger.isDebugEnabled()) {
198                     logger.debug("No key found");
199                 }
200             }
201 
202             return pk;
203         } finally {
204             if (pemParser != null) {
205                 try {
206                     pemParser.close();
207                 } catch (Exception exception) {
208                     logger.debug("Failed closing pem parser", exception);
209                 }
210             }
211         }
212     }
213 
214     private static PEMParser newParser(File keyFile) throws FileNotFoundException {
215         return new PEMParser(new FileReader(keyFile));
216     }
217 
218     private static PEMParser newParser(InputStream keyInputStream) {
219         return new PEMParser(new InputStreamReader(keyInputStream, CharsetUtil.US_ASCII));
220     }
221 
222     private BouncyCastlePemReader() { }
223 }