/* * Copyright (c) 2025, 2026, 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 java.security; import jdk.internal.javac.PreviewFeature; import jdk.internal.ref.CleanerFactory; import sun.security.pkcs.PKCS8Key; import sun.security.rsa.RSAPrivateCrtKeyImpl; import sun.security.util.KeyUtil; import sun.security.util.Pem; import javax.crypto.EncryptedPrivateKeyInfo; import javax.crypto.CryptoException; import javax.crypto.spec.PBEKeySpec; import java.io.*; import java.lang.ref.Reference; import java.nio.charset.StandardCharsets; import java.security.cert.*; import java.security.spec.*; import java.util.Objects; /** * {@code PEMDecoder} implements a decoder for Privacy-Enhanced Mail (PEM) data. * PEM is a textual encoding used to store and transfer cryptographic * objects, such as asymmetric keys, certificates, and certificate revocation * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of * Base64-encoded content enclosed by a type-identifying header * and footer. * *

The {@link #decode(String)} and {@link #decode(InputStream)} methods * return an instance of a class that matches the PEM type and implements * {@link BinaryEncodable}, as follows: *

* When used with a {@code PEMDecoder} instance configured for decryption: * * *

If the PEM type has no corresponding class, {@code decode(String)} and * {@code decode(InputStream)} will return a {@code PEM} object. * *

The {@link #decode(String, Class)} and {@link #decode(InputStream, Class)} * methods accept a class parameter specifying the desired {@code BinaryEncodable} * type. These methods avoid the need for casting and are useful when multiple * representations are possible. For example, if the PEM contains both public and * private keys, specifying {@code PrivateKey.class} returns only the private key. * If {@code X509EncodedKeySpec.class} is provided, the public key encoding is * returned as a {@code X509EncodedKeySpec}. To retrieve a {@link PEM} object, * use {@code PEM.class}. If the specified class does not * match the PEM content, a {@code ClassCastException} is thrown. * *

In addition to the types listed above, these methods support the * following PEM types and {@code BinaryEncodable} classes when specified as * parameters: *

* When used with a {@code PEMDecoder} instance configured for decryption: * * *

A new {@code PEMDecoder} instance is created when configured * with {@link #withFactoriesOf(Provider)} or {@link #withDecryption(char[])}. * The {@link #withFactoriesOf(Provider)} method uses the specified provider when * obtaining {@link KeyFactory} and {@link CertificateFactory} instances used * during decoding. The {@link #withDecryption(char[])} method configures the * decoder to decrypt and decode encrypted private key PEM data using the given * password. If decryption fails, a {@link CryptoException} is thrown. * If an encrypted PEM is processed by a decoder not configured * for decryption, an {@link EncryptedPrivateKeyInfo} is returned. * A {@code PEMDecoder} configured for decryption can also decode unencrypted PEM. * *

The {@code BinaryEncodable} interface may evolve. When using a decode method * with {@code switch}, always include a {@code default} case rather than * relying on the classes specified in the permits clause to remain fixed. * An exhaustive {@code switch} may result in a {@link MatchException}. * *

This class is immutable and thread-safe. * *

Example: decode a private key: * {@snippet lang = java: * PEMDecoder pd = PEMDecoder.of(); * PrivateKey priKey = pd.decode(priKeyPEM, PrivateKey.class); * } * *

Example: configure decryption and a factory provider: * {@snippet lang = java: * PEMDecoder pd = PEMDecoder.of().withDecryption(password). * withFactoriesOf(provider); * BinaryEncodable pemData = pd.decode(privKeyPEM); *} * * @implNote This implementation decodes non-encrypted RSA PRIVATE KEY as {@code PrivateKey}, * X509 CERTIFICATE and X.509 CERTIFICATE as {@code X509Certificate}, and CRL as * {@code X509CRL}. Other implementations may recognize additional PEM types. * * @see PEMEncoder * @see PEM * @see EncryptedPrivateKeyInfo * * @spec https://www.rfc-editor.org/info/rfc1421 * RFC 1421: Privacy Enhancement for Internet Electronic Mail * @spec https://www.rfc-editor.org/info/rfc5958 * RFC 5958: Asymmetric Key Packages * @spec https://www.rfc-editor.org/info/rfc7468 * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public final class PEMDecoder { private final Provider factory; private final PBEKeySpec keySpec; // Singleton instance for PEMDecoder private final static PEMDecoder PEM_DECODER = new PEMDecoder(null, null); /** * Creates an instance with a specific provider and/or password. * @param withFactory Key/Certificate factory provider * @param withKeySpec PBEKeySpec for EncryptedPrivateKeyInfo decryption */ private PEMDecoder(Provider withFactory, PBEKeySpec withKeySpec) { keySpec = withKeySpec; factory = withFactory; if (withKeySpec != null) { final var k = this.keySpec; CleanerFactory.cleaner().register(this, k::clearPassword); } } /** * Returns the default {@code PEMDecoder} instance. * * @return the default {@code PEMDecoder} */ public static PEMDecoder of() { return PEM_DECODER; } /** * After the header, footer, and base64 have been separated, identify the * header and footer and proceed with decoding the base64 for the * appropriate type. */ private BinaryEncodable decode(PEM pem) { try { return switch (pem.type()) { case Pem.PUBLIC_KEY -> { X509EncodedKeySpec spec = new X509EncodedKeySpec(pem.decode()); yield getKeyFactory( KeyUtil.getAlgorithm(spec.getEncoded())). generatePublic(spec); } case Pem.PRIVATE_KEY -> { BinaryEncodable d; PKCS8Key p8key = null; PKCS8EncodedKeySpec p8spec = null; byte[] encoding = pem.decode(); try { p8key = new PKCS8Key(encoding); String algo = p8key.getAlgorithm(); KeyFactory kf = getKeyFactory(algo); p8spec = new PKCS8EncodedKeySpec(encoding, algo); d = kf.generatePrivate(p8spec); // Look for a public key inside the pkcs8 encoding. if (p8key.getPubKeyEncoded() != null) { // Check if this is a OneAsymmetricKey encoding X509EncodedKeySpec spec = new X509EncodedKeySpec( p8key.getPubKeyEncoded(), algo); yield new KeyPair(getKeyFactory(algo). generatePublic(spec), (PrivateKey) d); } else if (d instanceof PKCS8Key p8 && p8.getPubKeyEncoded() != null) { // If the KeyFactory decoded an algorithm-specific // encodings, look for the public key again. X509EncodedKeySpec spec = new X509EncodedKeySpec( p8.getPubKeyEncoded(), algo); yield new KeyPair(getKeyFactory(algo). generatePublic(spec), (PrivateKey) d); } else { // No public key, return the private key. yield d; } } finally { KeyUtil.clear(encoding, p8spec, p8key); } } case Pem.ENCRYPTED_PRIVATE_KEY -> { byte[] p8 = null; var ekpi = new EncryptedPrivateKeyInfo(pem.decode()); if (keySpec == null) { yield ekpi; } try { p8 = Pem.decryptEncoding(ekpi, keySpec); } catch (GeneralSecurityException e) { throw new CryptoException(e); } try { yield Pem.toPKCS8Encodable(p8, factory); } finally { Reference.reachabilityFence(this); KeyUtil.clear(p8); } } case Pem.CERTIFICATE, Pem.X509_CERTIFICATE, Pem.X_509_CERTIFICATE -> { CertificateFactory cf = getCertFactory("X509"); yield (X509Certificate) cf.generateCertificate( new ByteArrayInputStream(pem.decode())); } case Pem.X509_CRL, Pem.CRL -> { CertificateFactory cf = getCertFactory("X509"); yield (X509CRL) cf.generateCRL( new ByteArrayInputStream(pem.decode())); } case Pem.RSA_PRIVATE_KEY -> { KeyFactory kf = getKeyFactory("RSA"); yield kf.generatePrivate( RSAPrivateCrtKeyImpl.getKeySpec(pem.decode())); } default -> pem; }; } catch (GeneralSecurityException | IOException e) { throw new IllegalArgumentException(e); } } /** * Decodes and returns a {@code BinaryEncodable} from the given {@code String}. * *

This method reads the {@code String} until PEM data is found * or the end of the {@code String} is reached. If no PEM data is found, * an {@code IllegalArgumentException} is thrown. * *

A {@code BinaryEncodable} is returned that best represents the * decoded content. If the PEM type is not supported, a {@code PEM} object is * returned containing the type identifier, Base64-encoded data, and any * leading data preceding the PEM header. For {@code BinaryEncodable} types * other than {@code PEM}, leading data is ignored. * *

The input is interpreted as * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. * * @param str a {@code String} containing PEM data * @return a {@code BinaryEncodable} * @throws IllegalArgumentException if decoding fails or no PEM data is found * @throws NullPointerException if {@code str} is {@code null} * @throws CryptoException if an error occurs during decryption * * @since 27 */ public BinaryEncodable decode(String str) { Objects.requireNonNull(str); byte[] encoding = null; try { encoding = str.getBytes(StandardCharsets.UTF_8); return decode(new ByteArrayInputStream(encoding)); } catch (IOException e) { // With all data contained in the String, there are no IO ops. throw new IllegalArgumentException(e); } finally { KeyUtil.clear(encoding); } } /** * Decodes and returns a {@code BinaryEncodable} from the given * {@code InputStream}. * *

This method reads from the {@code InputStream} until the end of * a PEM footer or the end of the stream. If an I/O error occurs, * the stream position may become inconsistent. Further decoding * operations on the same {@code InputStream} are not recommended. * *

A {@code BinaryEncodable} is returned that best represents the * decoded content. If the PEM type is not supported, a {@code PEM} object is * returned containing the type identifier, Base64-encoded data, and any * leading data preceding the PEM header. For {@code BinaryEncodable} types * other than {@code PEM}, leading data is ignored. * *

If no PEM data is found, an {@code EOFException} is thrown. * * @param is {@code InputStream} containing PEM data * @return a {@code BinaryEncodable} * @throws IOException if an I/O error occurs or PEM syntax is invalid * @throws EOFException if no PEM data is found or the stream ends unexpectedly * @throws IllegalArgumentException if decoding fails * @throws NullPointerException if {@code InputStream} is {@code null} * @throws CryptoException if an error occurs during decryption * * @since 27 */ public BinaryEncodable decode(InputStream is) throws IOException { Objects.requireNonNull(is); PEM pem = Pem.readPEM(is); BinaryEncodable be = null; try { be = decode(pem); return be; } finally { if (be != pem) { pem.clear(); } } } /** * Decodes and returns a {@code BinaryEncodable} of the specified class from * the given PEM string. * *

{@code tClass} must be an appropriate class for the PEM type. * *

This method reads the {@code String} until PEM data is found or the end * of the {@code String} is reached. If no PEM data is found, an * {@code IllegalArgumentException} is thrown. * *

If {@code tClass} is {@code PEM.class}, a {@code PEM} object is returned * containing the type identifier, Base64-encoded data, and any leading data * preceding the PEM header. For {@code BinaryEncodable} types other than * {@code PEM}, leading data is ignored. * *

The input is interpreted as * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. * * @param class type parameter that extends {@code BinaryEncodable} * @param str the {@code String} containing PEM data * @param tClass the returned object class that extends or implements * {@code BinaryEncodable} * @return a {@code BinaryEncodable} specified by {@code tClass} * @throws IllegalArgumentException on error in decoding or no PEM data found * @throws ClassCastException if {@code tClass} does not represent the PEM type * @throws NullPointerException if any input values are {@code null} * @throws CryptoException if an error occurs during decryption * * @since 27 */ public S decode(String str, Class tClass) { Objects.requireNonNull(str); byte[] encoding = null; try { encoding = str.getBytes(StandardCharsets.UTF_8); return decode(new ByteArrayInputStream(encoding), tClass); } catch (IOException e) { // With all data contained in the String, there are no IO ops. throw new IllegalArgumentException(e); } finally { KeyUtil.clear(encoding); } } /** * Decodes and returns a {@code BinaryEncodable} of the specified class from * the given {@code InputStream}. * *

{@code tClass} must be an appropriate class for the PEM type. * *

This method reads from the {@code InputStream} until the end of * a PEM footer or the end of the stream. If an I/O error occurs, * the stream position may become inconsistent. Further decoding * operations on the same {@code InputStream} are not recommended. * *

If {@code tClass} is {@code PEM.class}, a {@code PEM} object is returned * containing the type identifier, Base64-encoded data, and any leading data * preceding the PEM header. For {@code BinaryEncodable} types other than * {@code PEM}, leading data is ignored. * *

If no PEM data is found, an {@code EOFException} is thrown. * * @param class type parameter that extends {@code BinaryEncodable} * @param is an {@code InputStream} containing PEM data * @param tClass the returned object class that extends or implements * {@code BinaryEncodable} * @return a {@code BinaryEncodable} of type {@code tClass} * @throws IOException if an I/O error occurs or PEM syntax is invalid * @throws EOFException if no PEM data is found or the stream ends unexpectedly * @throws IllegalArgumentException if decoding fails * @throws ClassCastException if {@code tClass} does not represent the PEM type * @throws NullPointerException if any input values are {@code null} * @throws CryptoException if an error occurs during decryption * * @see #decode(InputStream) * @see #decode(String, Class) * * @since 27 */ public S decode(InputStream is, Class tClass) throws IOException { Objects.requireNonNull(is); Objects.requireNonNull(tClass); PEM pem = Pem.readPEM(is); if (tClass == PEM.class) { return tClass.cast(pem); } else if (tClass == BinaryEncodable.class) { pem.clear(); throw new ClassCastException("BinaryEncodable is not a PEM type"); } BinaryEncodable so; try { so = decode(pem); } finally { pem.clear(); } /* * If the object is a KeyPair, check if the tClass is set to class * specific to a private or public key. Because PKCS8v2 can be a * KeyPair, it is possible for someone to assume all their PEM private * keys are only PrivateKey and not KeyPair. */ if (so instanceof KeyPair kp) { if ((PrivateKey.class).isAssignableFrom(tClass) || (PKCS8EncodedKeySpec.class).isAssignableFrom(tClass)) { so = kp.getPrivate(); } if ((PublicKey.class).isAssignableFrom(tClass) || (X509EncodedKeySpec.class).isAssignableFrom(tClass)) { so = kp.getPublic(); if (kp.getPrivate() instanceof PKCS8Key p8Key) { KeyUtil.clear(p8Key); } } } /* * KeySpec use getKeySpec after the Key has been generated. Even though * returning a binary encoding after the Base64 decoding is ok when the * user wants PKCS8EncodedKeySpec, generating the key verifies the * binary encoding and allows the KeyFactory to use the provider's * KeySpec() */ if ((EncodedKeySpec.class).isAssignableFrom(tClass) && so instanceof Key key) { try { // unchecked suppressed as we know tClass comes from KeySpec // KeyType not relevant here. We just want KeyFactory if ((PKCS8EncodedKeySpec.class).isAssignableFrom(tClass)) { so = getKeyFactory(key.getAlgorithm()). getKeySpec(key, PKCS8EncodedKeySpec.class); } else if ((X509EncodedKeySpec.class).isAssignableFrom(tClass)) { so = getKeyFactory(key.getAlgorithm()) .getKeySpec(key, X509EncodedKeySpec.class); } else { throw new ClassCastException("Invalid KeySpec"); } } catch (InvalidKeySpecException e) { throw new ClassCastException("Invalid KeySpec " + "specified: " + tClass.getName() + " for key " + key.getClass().getName()); } finally { if (key instanceof PKCS8Key p8Key) { KeyUtil.clear(p8Key); } } } return tClass.cast(so); } private KeyFactory getKeyFactory(String algorithm) { if (algorithm == null || algorithm.isEmpty()) { throw new IllegalArgumentException("No algorithm found in " + "the encoding"); } try { if (factory == null) { return KeyFactory.getInstance(algorithm); } return KeyFactory.getInstance(algorithm, factory); } catch (GeneralSecurityException e) { throw new IllegalArgumentException(e); } } // Convenience method to avoid provider getInstance checks clutter private CertificateFactory getCertFactory(String algorithm) { try { if (factory == null) { return CertificateFactory.getInstance(algorithm); } return CertificateFactory.getInstance(algorithm, factory); } catch (GeneralSecurityException e) { throw new IllegalArgumentException(e); } } /** * Returns a copy of this {@code PEMDecoder} instance that uses * {@code KeyFactory} and {@code CertificateFactory} implementations * from the specified {@code Provider} to produce cryptographic objects. * Any errors using the {@code Provider} will occur during decoding. * * @param provider the factory {@code Provider} * @return a new {@code PEMDecoder} instance configured with the {@code Provider} * @throws NullPointerException if {@code provider} is {@code null} * * @since 27 */ public PEMDecoder withFactoriesOf(Provider provider) { Objects.requireNonNull(provider); if (keySpec == null) { return new PEMDecoder(provider, null); } char[] passwd = keySpec.getPassword(); try { return new PEMDecoder(provider, new PBEKeySpec(passwd)); } finally { KeyUtil.clear(passwd); } } /** * Returns a copy of this {@code PEMDecoder} that decodes and decrypts * encrypted private keys using the specified password. * Unencrypted PEM can also be decoded by the returned instance. * * @param password the password to decrypt the encrypted PEM data. This array * is cloned and stored in the new instance. * @return a new {@code PEMDecoder} instance configured for decryption * @throws NullPointerException if {@code password} is {@code null} */ public PEMDecoder withDecryption(char[] password) { Objects.requireNonNull(password); return new PEMDecoder(factory, new PBEKeySpec(password)); } }