001/* 002 * Copyright 2021-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2021-2022 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-2022 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}