mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-14 18:03:44 +00:00
473 lines
17 KiB
Java
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;
|
|
}
|
|
|
|
}
|