2025-11-14 21:08:36 +00:00

473 lines
17 KiB
Java

/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.util;
import sun.security.pkcs.PKCS8Key;
import sun.security.x509.AlgorithmId;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* A utility class for PEM format encoding.
*/
public class Pem {
private static final char WS = 0x20; // Whitespace
private static final byte[] CRLF = new byte[] {'\r', '\n'};
// Default algorithm from jdk.epkcs8.defaultAlgorithm in java.security
public static final String DEFAULT_ALGO;
// Pattern matching for EKPI operations
private static final Pattern PBE_PATTERN;
// Pattern matching for stripping whitespace.
private static final Pattern STRIP_WHITESPACE_PATTERN;
// Lazy initialized PBES2 OID value
private static ObjectIdentifier PBES2OID;
// Lazy initialized singleton encoder.
private static Base64.Encoder b64Encoder;
static {
String algo = Security.getProperty("jdk.epkcs8.defaultAlgorithm");
DEFAULT_ALGO = (algo == null || algo.isBlank()) ?
"PBEWithHmacSHA256AndAES_128" : algo;
PBE_PATTERN = Pattern.compile("^PBEWith.*And.*",
Pattern.CASE_INSENSITIVE);
STRIP_WHITESPACE_PATTERN = Pattern.compile("\\s+");
}
public static final String CERTIFICATE = "CERTIFICATE";
public static final String X509_CRL = "X509 CRL";
public static final String ENCRYPTED_PRIVATE_KEY = "ENCRYPTED PRIVATE KEY";
public static final String PRIVATE_KEY = "PRIVATE KEY";
public static final String RSA_PRIVATE_KEY = "RSA PRIVATE KEY";
public static final String PUBLIC_KEY = "PUBLIC KEY";
// old PEM types per RFC 7468
public static final String X509_CERTIFICATE = "X509 CERTIFICATE";
public static final String X_509_CERTIFICATE = "X.509 CERTIFICATE";
public static final String CRL = "CRL";
/**
* Decodes a PEM-encoded block.
*
* @param input the input string, according to RFC 1421, can only contain
* characters in the base-64 alphabet and whitespaces.
* @return the decoded bytes
*/
public static byte[] decode(String input) {
byte[] src = STRIP_WHITESPACE_PATTERN.matcher(input).replaceAll("").
getBytes(StandardCharsets.ISO_8859_1);
return Base64.getDecoder().decode(src);
}
/**
* Return the OID for a given PBE algorithm. PBES1 has an OID for each
* algorithm, while PBES2 has one OID for everything that complies with
* the formatting. Therefore, if the algorithm is not PBES1, it will
* return PBES2. Cipher will determine if this is a valid PBE algorithm.
* PBES2 specifies AES as the cipher algorithm, but any block cipher could
* be supported.
*/
public static ObjectIdentifier getPBEID(String algorithm) {
// Verify pattern matches PBE Standard Name spec
if (!PBE_PATTERN.matcher(algorithm).matches()) {
throw new IllegalArgumentException("Invalid algorithm format.");
}
// Return the PBES1 OID if it matches
try {
return AlgorithmId.get(algorithm).getOID();
} catch (NoSuchAlgorithmException e) {
// fall-through
}
// Lazy initialize
if (PBES2OID == null) {
try {
// Set to the hardcoded OID in KnownOID.java
PBES2OID = AlgorithmId.get("PBES2").getOID();
} catch (NoSuchAlgorithmException e) {
// Should never fail.
throw new IllegalArgumentException(e);
}
}
return PBES2OID;
}
/*
* RFC 7468 has some rules what generators should return given a historical
* type name. This converts read in PEM to the RFC. Change the type to
* be uniform is likely to help apps from not using all 3 certificate names.
*/
private static String typeConverter(String type) {
return switch (type) {
case Pem.X509_CERTIFICATE, Pem.X_509_CERTIFICATE -> Pem.CERTIFICATE;
case Pem.CRL -> Pem.X509_CRL;
default -> type;
};
}
/**
* Read the PEM text and return it in it's three components: header,
* base64, and footer.
*
* The header begins processing when "-----B" is read. At that point
* exceptions will be thrown for syntax errors.
*
* The method will leave the stream after reading the end of line of the
* footer or end of file
* @param is an InputStream
* @param shortHeader if true, the hyphen length is 4 because the first
* hyphen is assumed to have been read. This is needed
* for the CertificateFactory X509 implementation.
* @return a new PEMRecord
* @throws IOException on IO errors or PEM syntax errors that leave
* the read position not at the end of a PEM block
* @throws EOFException when at the unexpected end of the stream
* @throws IllegalArgumentException when a PEM syntax error occurs,
* but the read position in the stream is at the end of the block, so
* future reads can be successful.
*/
public static PEM readPEM(InputStream is, boolean shortHeader)
throws IOException {
Objects.requireNonNull(is);
int hyphen = (shortHeader ? 1 : 0);
int eol = 0;
ByteArrayOutputStream os = new ByteArrayOutputStream(6);
// Find 5 hyphens followed by a 'B' to start processing the header.
boolean headerStarted = false;
do {
int d = is.read();
switch (d) {
case '-' -> hyphen++;
case -1 -> {
if (os.size() == 0) {
throw new EOFException("No data available");
}
throw new EOFException("No PEM data found");
}
case 'B' -> {
if (hyphen == 5) {
headerStarted = true;
} else {
hyphen = 0;
}
}
default -> hyphen = 0;
}
os.write(d);
} while (!headerStarted);
StringBuilder sb = new StringBuilder(64);
sb.append("-----B");
hyphen = 0;
int c;
// Get header definition until first hyphen
do {
switch (c = is.read()) {
case '-' -> hyphen++;
case -1 -> throw new EOFException("Input ended prematurely");
case '\n', '\r' -> throw new IOException("Incomplete header");
default -> sb.append((char) c);
}
} while (hyphen == 0);
// Verify header ending with 5 hyphens.
do {
switch (is.read()) {
case '-' -> hyphen++;
default ->
throw new IOException("Incomplete header");
}
} while (hyphen < 5);
sb.append("-----");
String header = sb.toString();
if (header.length() < 16 || !header.startsWith("-----BEGIN ") ||
!header.endsWith("-----")) {
throw new IOException("Illegal header: " + header);
}
hyphen = 0;
sb = new StringBuilder(1024);
// Determine the line break using the char after the last hyphen
switch (is.read()) {
case WS -> {} // skip whitespace
case '\r' -> {
c = is.read();
if (c == '\n') {
eol = '\n';
} else {
eol = '\r';
sb.append((char) c);
}
}
case '\n' -> eol = '\n';
default ->
throw new IOException("No EOL character found");
}
// Read data until we find the first footer hyphen.
do {
switch (c = is.read()) {
case -1 ->
throw new EOFException("Incomplete header");
case '-' -> hyphen++;
case WS, '\t', '\r', '\n' -> {} // skip whitespace and tab
default -> sb.append((char) c);
}
} while (hyphen == 0);
String data = sb.toString();
// Verify footer starts with 5 hyphens.
do {
switch (is.read()) {
case '-' -> hyphen++;
case -1 -> throw new EOFException("Input ended prematurely");
default -> throw new IOException("Incomplete footer");
}
} while (hyphen < 5);
hyphen = 0;
sb = new StringBuilder(64);
sb.append("-----");
// Look for Complete header by looking for the end of the hyphens
do {
switch (c = is.read()) {
case '-' -> hyphen++;
case -1 -> throw new EOFException("Input ended prematurely");
default -> sb.append((char) c);
}
} while (hyphen == 0);
// Verify ending with 5 hyphens.
do {
switch (is.read()) {
case '-' -> hyphen++;
case -1 -> throw new EOFException("Input ended prematurely");
default -> throw new IOException("Incomplete footer");
}
} while (hyphen < 5);
while ((c = is.read()) != eol && c != -1 && c != WS) {
// skip when eol is '\n', the line separator is likely "\r\n".
if (c == '\r') {
continue;
}
throw new IOException("Invalid PEM format: " +
"No EOL char found in footer: 0x" +
HexFormat.of().toHexDigits((byte) c));
}
sb.append("-----");
String footer = sb.toString();
if (footer.length() < 14 || !footer.startsWith("-----END ") ||
!footer.endsWith("-----")) {
// Not an IOE because the read pointer is correctly at the end.
throw new IOException("Illegal footer: " + footer);
}
// Verify the object type in the header and the footer are the same.
String headerType = header.substring(11, header.length() - 5);
String footerType = footer.substring(9, footer.length() - 5);
if (!headerType.equals(footerType)) {
throw new IOException("Header and footer do not " +
"match: " + headerType + " " + footerType);
}
// If there was data before finding the 5 dashes of the PEM header,
// backup 5 characters and save that data.
byte[] preData = null;
if (os.size() > 6) {
preData = Arrays.copyOf(os.toByteArray(), os.size() - 6);
}
return new PEM(typeConverter(headerType), data, preData);
}
public static PEM readPEM(InputStream is) throws IOException {
return readPEM(is, false);
}
private static String pemEncoded(String type, String base64) {
return
"-----BEGIN " + type + "-----\r\n" +
base64 + (!base64.endsWith("\n") ? "\r\n" : "") +
"-----END " + type + "-----\r\n";
}
/**
* Construct a String-based encoding based off the type. leadingData
* is not used with this method.
* @return PEM in a string
*/
public static String pemEncoded(String type, byte[] der) {
if (b64Encoder == null) {
b64Encoder = Base64.getMimeEncoder(64, CRLF);
}
return pemEncoded(type, b64Encoder.encodeToString(der));
}
/**
* Construct a String-based encoding based off the type. leadingData
* is not used with this method.
* @return PEM in a string
*/
public static String pemEncoded(PEM pem) {
String p = pem.content().replaceAll("(.{64})", "$1\r\n");
return pemEncoded(pem.type(), p);
}
/*
* Get PKCS8 encoding from an encrypted private key encoding.
*/
public static byte[] decryptEncoding(byte[] encoded, char[] password)
throws GeneralSecurityException {
EncryptedPrivateKeyInfo ekpi;
Objects.requireNonNull(password, "password cannot be null");
PBEKeySpec keySpec = new PBEKeySpec(password);
try {
ekpi = new EncryptedPrivateKeyInfo(encoded);
return decryptEncoding(ekpi, keySpec);
} catch (IOException e) {
throw new IllegalArgumentException(e);
} finally {
keySpec.clearPassword();
}
}
public static byte[] decryptEncoding(EncryptedPrivateKeyInfo ekpi, PBEKeySpec keySpec)
throws NoSuchAlgorithmException, InvalidKeyException {
PKCS8EncodedKeySpec p8KeySpec = null;
try {
SecretKeyFactory skf = SecretKeyFactory.getInstance(ekpi.getAlgName());
p8KeySpec = ekpi.getKeySpec(skf.generateSecret(keySpec));
return p8KeySpec.getEncoded();
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException(e);
} finally {
KeyUtil.clear(p8KeySpec);
}
}
/**
* With a given PKCS8 encoding, construct a PrivateKey or KeyPair. A
* KeyPair is returned if requested and the encoding has a public key;
* otherwise, a PrivateKey is returned.
*
* @param encoded PKCS8 encoding
* @param pair set to true for returning a KeyPair, if possible. Otherwise,
* return a PrivateKey
* @param provider KeyFactory provider
*/
public static DEREncodable toDEREncodable(byte[] encoded, boolean pair,
Provider provider) throws InvalidKeyException {
PrivateKey privKey;
PublicKey pubKey = null;
PKCS8EncodedKeySpec p8KeySpec;
PKCS8Key p8key = new PKCS8Key(encoded);
KeyFactory kf;
try {
p8KeySpec = new PKCS8EncodedKeySpec(encoded);
} catch (NullPointerException e) {
p8key.clear();
throw new InvalidKeyException("No encoding found", e);
}
try {
if (provider == null) {
kf = KeyFactory.getInstance(p8key.getAlgorithm());
} else {
kf = KeyFactory.getInstance(p8key.getAlgorithm(), provider);
}
} catch (NoSuchAlgorithmException e) {
KeyUtil.clear(p8KeySpec, p8key);
throw new InvalidKeyException("Unable to find the algorithm: " +
p8key.getAlgorithm(), e);
}
try {
privKey = kf.generatePrivate(p8KeySpec);
// Only want the PrivateKey? then return it.
if (!pair) {
return privKey;
}
if (p8key.hasPublicKey()) {
// PKCS8Key.decode() has extracted the public key already
pubKey = kf.generatePublic(
new X509EncodedKeySpec(p8key.getPubKeyEncoded()));
} else {
// In case decode() could not read the public key, the
// KeyFactory can try. Failure is ok as there may not
// be a public key in the encoding.
try {
pubKey = kf.generatePublic(p8KeySpec);
} catch (InvalidKeySpecException e) {
// ignore
}
}
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException(e);
} finally {
KeyUtil.clear(p8KeySpec, p8key);
}
if (pair && pubKey != null) {
return new KeyPair(pubKey, privKey);
}
return privKey;
}
}