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}