001/*
002 * Copyright 2021-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2021-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2021-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util.ssl.cert;
037
038
039
040import java.io.BufferedReader;
041import java.io.Closeable;
042import java.io.File;
043import java.io.FileInputStream;
044import java.io.IOException;
045import java.io.InputStream;
046import java.io.InputStreamReader;
047
048import com.unboundid.util.Base64;
049import com.unboundid.util.Debug;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055
056import static com.unboundid.util.ssl.cert.CertMessages.*;
057
058
059
060/**
061 * This class provides a mechanism for reading a PEM-encoded PKCS #8 private key
062 * from a specified file.  While it is generally expected that a private key
063 * file will contain only a single key, it is possible to read multiple keys
064 * from the same file.  Each private key should consist of the following:
065 * <UL>
066 *   <LI>A line containing only the string "-----BEGIN PRIVATE KEY-----" or
067 *       ""-----BEGIN RSA PRIVATE KEY-----.</LI>
068 *   <LI>One or more lines representing the base64-encoded representation of the
069 *       bytes that comprise the PKCS #8 private key.</LI>
070 *   <LI>A line containing only the string "-----END PRIVATE KEY-----" or
071 *       ""-----END RSA PRIVATE KEY-----.</LI>
072 * </UL>
073 * <BR><BR>
074 * Any spaces that appear at the beginning or end of each line will be ignored.
075 * Empty lines and lines that start with the octothorpe (#) character will also
076 * be ignored.
077 */
078@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
079public final class PKCS8PEMFileReader
080       implements Closeable
081{
082  /**
083   * The header string that should appear on a line by itself before the
084   * base64-encoded representation of the bytes that comprise an encrypted
085   * PKCS #8 private key.
086   */
087  @NotNull public static final String BEGIN_ENCRYPTED_PRIVATE_KEY_HEADER =
088       "-----BEGIN ENCRYPTED PRIVATE KEY-----";
089
090
091
092  /**
093   * The header string that should appear on a line by itself before the
094   * base64-encoded representation of the bytes that comprise a PKCS #8 private
095   * key.
096   */
097  @NotNull public static final String BEGIN_PRIVATE_KEY_HEADER =
098       "-----BEGIN PRIVATE KEY-----";
099
100
101
102  /**
103   * An alternative begin header string that may appear on a line by itself for
104   * cases in which the certificate uses an RSA key pair.
105   */
106  @NotNull public static final String BEGIN_RSA_PRIVATE_KEY_HEADER =
107       "-----BEGIN RSA PRIVATE KEY-----";
108
109
110
111  /**
112   * The footer string that should appear on a line by itself after the
113   * base64-encoded representation of the bytes that comprise an encrypted
114   * PKCS #8 private key.
115   */
116  @NotNull public static final String END_ENCRYPTED_PRIVATE_KEY_FOOTER =
117       "-----END ENCRYPTED PRIVATE KEY-----";
118
119
120
121  /**
122   * The footer string that should appear on a line by itself after the
123   * base64-encoded representation of the bytes that comprise a PKCS #8 private
124   * key.
125   */
126  @NotNull public static final String END_PRIVATE_KEY_FOOTER =
127       "-----END PRIVATE KEY-----";
128
129
130
131  /**
132   * An alternative end footer string that may appear on a line by itself for
133   * cases in which the certificate uses an RSA key pair.
134   */
135  @NotNull public static final String END_RSA_PRIVATE_KEY_FOOTER =
136       "-----END RSA PRIVATE KEY-----";
137
138
139
140  // The reader that will be used to consume data from the PEM file.
141  @NotNull private final BufferedReader reader;
142
143
144
145  /**
146   * Creates a new PKCS #8 PEM file reader that will read private key
147   * information from the specified file.
148   *
149   * @param  pemFilePath  The path to the PEM file from which the private key
150   *                      should be read.  This must not be {@code null} and the
151   *                      file must exist.
152   *
153   * @throws  IOException  If a problem occurs while attempting to open the file
154   *                       for reading.
155   */
156  public PKCS8PEMFileReader(@NotNull final String pemFilePath)
157         throws IOException
158  {
159    this(new File(pemFilePath));
160  }
161
162
163
164  /**
165   * Creates a new PKCS #8 PEM file reader that will read private key
166   * information from the specified file.
167   *
168   * @param  pemFile  The PEM file from which the private key should be read.
169   *                  This must not be {@code null} and the file must
170   *                  exist.
171   *
172   * @throws  IOException  If a problem occurs while attempting to open the file
173   *                       for reading.
174   */
175  public PKCS8PEMFileReader(@NotNull final File pemFile)
176         throws IOException
177  {
178    this(new FileInputStream(pemFile));
179  }
180
181
182
183  /**
184   * Creates a new PKCS #8 PEM file reader that will read private key
185   * information from the provided input stream.
186   *
187   * @param  inputStream  The input stream from which the private key should
188   *                      be read.  This must not be {@code null} and it must be
189   *                      open for reading.
190   */
191  public PKCS8PEMFileReader(@NotNull final InputStream inputStream)
192  {
193    reader = new BufferedReader(new InputStreamReader(inputStream));
194  }
195
196
197
198  /**
199   * Reads the next private key from the PEM file.  The private key must be
200   * unencrypted.
201   *
202   * @return  The private key that was read, or {@code null} if the end of the
203   *          file has been reached.
204   *
205   * @throws  IOException  If a problem occurs while trying to read data from
206   *                       the PEM file.
207   *
208   * @throws  CertException  If a problem occurs while trying to interpret data
209   *                         read from the PEM file as a PKCS #8 private key.
210   */
211  @Nullable()
212  public PKCS8PrivateKey readPrivateKey()
213         throws IOException, CertException
214  {
215    return readPrivateKey(null);
216  }
217
218
219
220  /**
221   * Reads the next private key from the PEM file.  The private key may
222   * optionally be encrypted.
223   *
224   * @param  encryptionPassword  The password used to encrypt the private key.
225   *                             It must not be {@code null} if the private key
226   *                             is encrypted.  It may be {@code null} if the
227   *                             private key is not encrypted.
228   *
229   * @return  The private key that was read, or {@code null} if the end of the
230   *          file has been reached.
231   *
232   * @throws  IOException  If a problem occurs while trying to read data from
233   *                       the PEM file.
234   *
235   * @throws  CertException  If a problem occurs while trying to interpret data
236   *                         read from the PEM file as a PKCS #8 private key.
237   */
238  @Nullable()
239  public PKCS8PrivateKey readPrivateKey(
240              @Nullable final char[] encryptionPassword)
241         throws IOException, CertException
242  {
243    boolean isEncrypted = false;
244    String beginLine = null;
245    final StringBuilder base64Buffer = new StringBuilder();
246
247    while (true)
248    {
249      final String line = reader.readLine();
250      if (line == null)
251      {
252        // We hit the end of the file.  If we read a begin header, then that's
253        // an error.
254        if (beginLine != null)
255        {
256          throw new CertException(ERR_PKCS8_PEM_READER_EOF_WITHOUT_END.get(
257               END_PRIVATE_KEY_FOOTER, beginLine));
258        }
259
260        return null;
261      }
262
263      final String trimmedLine = line.trim();
264      if (trimmedLine.isEmpty() || trimmedLine.startsWith("#"))
265      {
266        continue;
267      }
268
269      final String upperLine = StaticUtils.toUpperCase(trimmedLine);
270      if (BEGIN_ENCRYPTED_PRIVATE_KEY_HEADER.equals(upperLine) ||
271           BEGIN_PRIVATE_KEY_HEADER.equals(upperLine) ||
272           BEGIN_RSA_PRIVATE_KEY_HEADER.equals(upperLine))
273      {
274        if (beginLine != null)
275        {
276          throw new CertException(ERR_PKCS8_PEM_READER_REPEATED_BEGIN.get(
277               upperLine));
278        }
279        else
280        {
281          beginLine = upperLine;
282
283          if (BEGIN_ENCRYPTED_PRIVATE_KEY_HEADER.equals(upperLine))
284          {
285            isEncrypted = true;
286            if (encryptionPassword == null)
287            {
288              throw new CertException(
289                   ERR_PKCS8_PEM_READER_NO_PW_FOR_ENCRYPTED_KEY.get());
290            }
291          }
292        }
293      }
294      else if (END_ENCRYPTED_PRIVATE_KEY_FOOTER.equals(upperLine) ||
295           END_PRIVATE_KEY_FOOTER.equals(upperLine) ||
296           END_RSA_PRIVATE_KEY_FOOTER.equals(upperLine))
297      {
298        if (beginLine == null)
299        {
300          throw new CertException(ERR_PKCS8_PEM_READER_END_WITHOUT_BEGIN.get(
301               upperLine, beginLine));
302        }
303        else if (base64Buffer.length() == 0)
304        {
305          throw new CertException(ERR_PKCS8_PEM_READER_END_WITHOUT_DATA.get(
306               upperLine, beginLine));
307        }
308        else
309        {
310          final byte[] pkcs8Bytes;
311          if (isEncrypted)
312          {
313            final byte[] encryptedKeyBytes;
314            try
315            {
316              encryptedKeyBytes = Base64.decode(base64Buffer.toString());
317            }
318            catch (final Exception e)
319            {
320              Debug.debugException(e);
321              throw new CertException(
322                   ERR_PKCS8_PEM_READER_CANNOT_BASE64_DECODE.get(), e);
323            }
324
325            return PKCS8EncryptionHandler.decryptPrivateKey(
326                 encryptedKeyBytes, encryptionPassword);
327          }
328          else
329          {
330            try
331            {
332              pkcs8Bytes = Base64.decode(base64Buffer.toString());
333            }
334            catch (final Exception e)
335            {
336              Debug.debugException(e);
337              throw new CertException(
338                   ERR_PKCS8_PEM_READER_CANNOT_BASE64_DECODE.get(), e);
339            }
340          }
341
342          return new PKCS8PrivateKey(pkcs8Bytes);
343        }
344      }
345      else
346      {
347        if (beginLine == null)
348        {
349          throw new CertException(ERR_PKCS8_PEM_READER_DATA_WITHOUT_BEGIN.get(
350               BEGIN_PRIVATE_KEY_HEADER));
351        }
352
353        base64Buffer.append(trimmedLine);
354      }
355    }
356  }
357
358
359
360  /**
361   * Closes this PKCS #8 PEM file reader.
362   *
363   * @throws  IOException  If a problem is encountered while attempting to close
364   *                       the reader.
365   */
366  @Override()
367  public void close()
368         throws IOException
369  {
370    reader.close();
371  }
372}