001/*
002 * Copyright 2021 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2021 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 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 a PKCS #8 private
085   * key.
086   */
087  @NotNull public static final String BEGIN_PRIVATE_KEY_HEADER =
088       "-----BEGIN PRIVATE KEY-----";
089
090
091
092  /**
093   * An alternative begin header string that may appear on a line by itself for
094   * cases in which the certificate uses an RSA key pair.
095   */
096  @NotNull public static final String BEGIN_RSA_PRIVATE_KEY_HEADER =
097       "-----BEGIN RSA PRIVATE KEY-----";
098
099
100
101  /**
102   * The footer string that should appear on a line by itself after the
103   * base64-encoded representation of the bytes that comprise a PKCS #8 private
104   * key.
105   */
106  @NotNull public static final String END_PRIVATE_KEY_FOOTER =
107       "-----END PRIVATE KEY-----";
108
109
110
111  /**
112   * An alternative end footer string that may appear on a line by itself for
113   * cases in which the certificate uses an RSA key pair.
114   */
115  @NotNull public static final String END_RSA_PRIVATE_KEY_FOOTER =
116       "-----END RSA PRIVATE KEY-----";
117
118
119
120  // The reader that will be used to consume data from the PEM file.
121  @NotNull private final BufferedReader reader;
122
123
124
125  /**
126   * Creates a new PKCS #8 PEM file reader that will read private key
127   * information from the specified file.
128   *
129   * @param  pemFilePath  The path to the PEM file from which the private key
130   *                      should be read.  This must not be {@code null} and the
131   *                      file must exist.
132   *
133   * @throws  IOException  If a problem occurs while attempting to open the file
134   *                       for reading.
135   */
136  public PKCS8PEMFileReader(@NotNull final String pemFilePath)
137         throws IOException
138  {
139    this(new File(pemFilePath));
140  }
141
142
143
144  /**
145   * Creates a new PKCS #8 PEM file reader that will read private key
146   * information from the specified file.
147   *
148   * @param  pemFile  The PEM file from which the private key should be read.
149   *                  This must not be {@code null} and the file must
150   *                  exist.
151   *
152   * @throws  IOException  If a problem occurs while attempting to open the file
153   *                       for reading.
154   */
155  public PKCS8PEMFileReader(@NotNull final File pemFile)
156         throws IOException
157  {
158    this(new FileInputStream(pemFile));
159  }
160
161
162
163  /**
164   * Creates a new PKCS #8 PEM file reader that will read private key
165   * information from the provided input stream.
166   *
167   * @param  inputStream  The input stream from which the private key should
168   *                      be read.  This must not be {@code null} and it must be
169   *                      open for reading.
170   */
171  public PKCS8PEMFileReader(@NotNull final InputStream inputStream)
172  {
173    reader = new BufferedReader(new InputStreamReader(inputStream));
174  }
175
176
177
178  /**
179   * Reads the next private key from the PEM file.
180   *
181   * @return  The private key that was read, or {@code null} if the end of the
182   *          file has been reached.
183   *
184   * @throws  IOException  If a problem occurs while trying to read data from
185   *                       the PEM file.
186   *
187   * @throws  CertException  If a problem occurs while trying to interpret data
188   *                         read from the PEM file as a PKCS #8 private key.
189   */
190  @Nullable()
191  public PKCS8PrivateKey readPrivateKey()
192         throws IOException, CertException
193  {
194    String beginLine = null;
195    final StringBuilder base64Buffer = new StringBuilder();
196
197    while (true)
198    {
199      final String line = reader.readLine();
200      if (line == null)
201      {
202        // We hit the end of the file.  If we read a begin header, then that's
203        // an error.
204        if (beginLine != null)
205        {
206          throw new CertException(ERR_PKCS8_PEM_READER_EOF_WITHOUT_END.get(
207               END_PRIVATE_KEY_FOOTER, beginLine));
208        }
209
210        return null;
211      }
212
213      final String trimmedLine = line.trim();
214      if (trimmedLine.isEmpty() || trimmedLine.startsWith("#"))
215      {
216        continue;
217      }
218
219      final String upperLine = StaticUtils.toUpperCase(trimmedLine);
220      if (BEGIN_PRIVATE_KEY_HEADER.equals(upperLine) ||
221           BEGIN_RSA_PRIVATE_KEY_HEADER.equals(upperLine))
222      {
223        if (beginLine != null)
224        {
225          throw new CertException(ERR_PKCS8_PEM_READER_REPEATED_BEGIN.get(
226               upperLine));
227        }
228        else
229        {
230          beginLine = upperLine;
231        }
232      }
233      else if (END_PRIVATE_KEY_FOOTER.equals(upperLine) ||
234           END_RSA_PRIVATE_KEY_FOOTER.equals(upperLine))
235      {
236        if (beginLine == null)
237        {
238          throw new CertException(ERR_PKCS8_PEM_READER_END_WITHOUT_BEGIN.get(
239               upperLine, beginLine));
240        }
241        else if (base64Buffer.length() == 0)
242        {
243          throw new CertException(ERR_PKCS8_PEM_READER_END_WITHOUT_DATA.get(
244               upperLine, beginLine));
245        }
246        else
247        {
248          final byte[] pkcs8Bytes;
249          try
250          {
251            pkcs8Bytes = Base64.decode(base64Buffer.toString());
252          }
253          catch (final Exception e)
254          {
255            Debug.debugException(e);
256            throw new CertException(
257                 ERR_PKCS8_PEM_READER_CANNOT_BASE64_DECODE.get(), e);
258          }
259
260          return new PKCS8PrivateKey(pkcs8Bytes);
261        }
262      }
263      else
264      {
265        if (beginLine == null)
266        {
267          throw new CertException(ERR_PKCS8_PEM_READER_DATA_WITHOUT_BEGIN.get(
268               BEGIN_PRIVATE_KEY_HEADER));
269        }
270
271        base64Buffer.append(trimmedLine);
272      }
273    }
274  }
275
276
277
278  /**
279   * Closes this PKCS #8 PEM file reader.
280   *
281   * @throws  IOException  If a problem is encountered while attempting to close
282   *                       the reader.
283   */
284  @Override()
285  public void close()
286         throws IOException
287  {
288    reader.close();
289  }
290}