diff --git a/src/java.base/share/classes/java/security/DEREncodable.java b/src/java.base/share/classes/java/security/DEREncodable.java index 63c6a73ee52b5..1401336037c2c 100644 --- a/src/java.base/share/classes/java/security/DEREncodable.java +++ b/src/java.base/share/classes/java/security/DEREncodable.java @@ -47,7 +47,7 @@ * @see EncryptedPrivateKeyInfo * @see X509Certificate * @see X509CRL - * @see PEMRecord + * @see PEM * * @since 25 */ @@ -55,5 +55,5 @@ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public sealed interface DEREncodable permits AsymmetricKey, KeyPair, PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo, - X509Certificate, X509CRL, PEMRecord { + X509Certificate, X509CRL, PEM { } diff --git a/src/java.base/share/classes/java/security/PEM.java b/src/java.base/share/classes/java/security/PEM.java new file mode 100644 index 0000000000000..2068fb707dc1d --- /dev/null +++ b/src/java.base/share/classes/java/security/PEM.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 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 java.security; + +import jdk.internal.javac.PreviewFeature; + +import sun.security.util.Pem; + +import java.io.InputStream; +import java.util.Base64; +import java.util.Objects; + +/** + * {@code PEM} is a {@link DEREncodable} that represents Privacy-Enhanced + * Mail (PEM) data by its type and Base64-encoded content. + * + *

The {@link PEMDecoder#decode(String)} and + * {@link PEMDecoder#decode(InputStream)} methods return a {@code PEM} object + * when the data type cannot be represented by a cryptographic object. + * If you need access to the leading data of a PEM text, or want to + * handle the text content directly, use the decoding methods + * {@link PEMDecoder#decode(String, Class)} or + * {@link PEMDecoder#decode(InputStream, Class)} with {@code PEM.class} as an + * argument type. + * + *

A {@code PEM} object can be encoded back to its textual format by calling + * {@link #toString()} or by using the encode methods in {@link PEMEncoder}. + * + *

When constructing a {@code PEM} instance, both {@code type} and + * {@code content} must not be {@code null}. + * + *

No validation is performed during instantiation to ensure that + * {@code type} conforms to RFC 7468 or other legacy formats, that + * {@code content} is valid Base64 data, or that {@code content} matches the + * {@code type}. + + *

Common {@code type} values include, but are not limited to: + * CERTIFICATE, CERTIFICATE REQUEST, ATTRIBUTE CERTIFICATE, X509 CRL, PKCS7, + * CMS, PRIVATE KEY, ENCRYPTED PRIVATE KEY, and PUBLIC KEY. + * + *

{@code leadingData} is {@code null} if there is no data preceding the PEM + * header during decoding. {@code leadingData} can be useful for reading + * metadata that accompanies the PEM data. Because the value may represent a large + * amount of data, it is not defensively copied by the constructor, and the + * {@link #leadingData()} method does not return a clone. Modification of the + * passed-in or returned array changes the value stored in this record. + * + * @param type the type identifier from the PEM header, without PEM syntax + * labels; for example, for a public key, {@code type} would be + * "PUBLIC KEY" + * @param content the Base64-encoded data, excluding the PEM header and footer + * @param leadingData any non-PEM data that precedes the PEM header during + * decoding. This value may be {@code null}. + * + * @spec https://www.rfc-editor.org/info/rfc7468 + * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures + * + * @see PEMDecoder + * @see PEMEncoder + * + * @since 26 + */ +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public record PEM(String type, String content, byte[] leadingData) + implements DEREncodable { + + /** + * Creates a {@code PEM} instance with the specified parameters. + * + * @param type the PEM type identifier + * @param content the Base64-encoded data, excluding the PEM header and footer + * @param leadingData any non-PEM data read during the decoding process + * before the PEM header. This value may be {@code null}. + * @throws IllegalArgumentException if {@code type} is incorrectly formatted + * @throws NullPointerException if {@code type} or {@code content} is {@code null} + */ + public PEM { + Objects.requireNonNull(type, "\"type\" cannot be null."); + Objects.requireNonNull(content, "\"content\" cannot be null."); + + // With no validity checking on `type`, the constructor accept anything + // including lowercase. The onus is on the caller. + if (type.startsWith("-") || type.startsWith("BEGIN ") || + type.startsWith("END ")) { + throw new IllegalArgumentException("PEM syntax labels found. " + + "Only the PEM type identifier is allowed."); + } + } + + /** + * Creates a {@code PEM} instance with the specified type and content. This + * constructor sets {@code leadingData} to {@code null}. + * + * @param type the PEM type identifier + * @param content the Base64-encoded data, excluding the PEM header and footer + * @throws IllegalArgumentException if {@code type} is incorrectly formatted + * @throws NullPointerException if {@code type} or {@code content} is {@code null} + */ + public PEM(String type, String content) { + this(type, content, null); + } + + /** + * Returns the PEM formatted string containing the {@code type} and + * Base64-encoded {@code content}. {@code leadingData} is not included. + * + * @return the PEM text representation + */ + @Override + final public String toString() { + return Pem.pemEncoded(this); + } + + /** + * Returns a Base64-decoded byte array of {@code content}, using + * {@link Base64#getMimeDecoder()}. + * + * @return a decoded byte array + * @throws IllegalArgumentException if decoding fails + */ + final public byte[] decode() { + return Base64.getMimeDecoder().decode(content); + } +} diff --git a/src/java.base/share/classes/java/security/PEMDecoder.java b/src/java.base/share/classes/java/security/PEMDecoder.java index 0b959bf244031..7a4b7753876b9 100644 --- a/src/java.base/share/classes/java/security/PEMDecoder.java +++ b/src/java.base/share/classes/java/security/PEMDecoder.java @@ -27,6 +27,7 @@ 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; @@ -35,6 +36,7 @@ import javax.crypto.EncryptedPrivateKeyInfo; 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.*; @@ -43,96 +45,105 @@ /** * {@code PEMDecoder} implements a decoder for Privacy-Enhanced Mail (PEM) data. - * PEM is a textual encoding used to store and transfer security + * 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 a - * Base64-formatted binary encoding enclosed by a type-identifying header + * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of a + * Base64-encoded binary encoding enclosed by a type-identifying header * and footer. * - *

The {@linkplain #decode(String)} and {@linkplain #decode(InputStream)} - * methods return an instance of a class that matches the data - * type and implements {@link DEREncodable}. - * - *

The following lists the supported PEM types and the {@code DEREncodable} - * types that each are decoded as: + *

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

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

The {@code PublicKey} and {@code PrivateKey} types, an algorithm specific - * subclass is returned if the underlying algorithm is supported. For example an - * ECPublicKey and ECPrivateKey for Elliptic Curve keys. + *

For {@code PublicKey} and {@code PrivateKey} types, an algorithm-specific + * subclass is returned if the algorithm is supported. For example, an + * {@code ECPublicKey} or an {@code ECPrivateKey} for Elliptic Curve keys. * *

If the PEM type does not have a corresponding class, * {@code decode(String)} and {@code decode(InputStream)} will return a - * {@link PEMRecord}. + * {@code PEM} object. * - *

The {@linkplain #decode(String, Class)} and - * {@linkplain #decode(InputStream, Class)} methods take a class parameter - * which determines the type of {@code DEREncodable} that is returned. These - * methods are useful when extracting or changing the return class. - * For example, if the PEM contains both public and private keys, the - * class parameter can specify which to return. Use - * {@code PrivateKey.class} to return only the private key. - * If the class parameter is set to {@code X509EncodedKeySpec.class}, the - * public key will be returned in that format. Any type of PEM data can be - * decoded into a {@code PEMRecord} by specifying {@code PEMRecord.class}. - * If the class parameter doesn't match the PEM content, a - * {@linkplain ClassCastException} will be thrown. + *

The {@link #decode(String, Class)} and {@link #decode(InputStream, Class)} + * methods take a class parameter that specifies the type of {@code DEREncodable} + * to return. These methods are useful for avoiding casts when the PEM type is + * known, or when extracting a specific type if there is more than one option. + * For example, if the PEM contains both a public and private key, specifying + * {@code PrivateKey.class} returns only the private key. + * If the class parameter specifies {@code X509EncodedKeySpec.class}, the + * public key encoding is returned as an instance of {@code X509EncodedKeySpec} + * class. Any type of PEM data can be decoded into a {@code PEM} object by + * specifying {@code PEM.class}. If the class parameter 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 DEREncodable} 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 {@linkplain #withFactory(Provider)} and/or - * {@linkplain #withDecryption(char[])}. {@linkplain #withFactory(Provider)} - * configures the decoder to use only {@linkplain KeyFactory} and - * {@linkplain CertificateFactory} instances from the given {@code Provider}. - * {@linkplain #withDecryption(char[])} configures the decoder to decrypt all - * encrypted private key PEM data using the given password. - * Configuring an instance for decryption does not prevent decoding with - * unencrypted PEM. Any encrypted PEM that fails decryption - * will throw a {@link RuntimeException}. When an encrypted private key PEM is - * used with a decoder not configured for decryption, an - * {@link EncryptedPrivateKeyInfo} object is returned. + * with {@link #withFactory(Provider)} or {@link #withDecryption(char[])}. + * The {@link #withFactory(Provider)} method uses the specified provider + * to produce cryptographic objects from {@link KeyFactory} and + * {@link CertificateFactory}. The {@link #withDecryption(char[])} method configures the + * decoder to decrypt and decode encrypted private key PEM data using the given + * password. If decryption fails, an {@link IllegalArgumentException} is thrown. + * If an encrypted private key PEM is processed by a decoder not configured + * for decryption, an {@link EncryptedPrivateKeyInfo} object is returned. + * A {@code PEMDecoder} configured for decryption will decode unencrypted PEM. * - *

This class is immutable and thread-safe. + *

This class is immutable and thread-safe. * - *

Here is an example of decoding a {@code PrivateKey} object: + *

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

Here is an example of a {@code PEMDecoder} configured with decryption - * and a factory provider: + *

Example: configure decryption and a factory provider: * {@snippet lang = java: * PEMDecoder pd = PEMDecoder.of().withDecryption(password). - * withFactory(provider); - * byte[] pemData = pd.decode(privKey); + * withFactory(provider); + * DEREncodable pemData = pd.decode(privKeyPEM); * } * - * @implNote An implementation may support other PEM types and - * {@code DEREncodable} objects. This implementation additionally supports - * the following PEM types: {@code X509 CERTIFICATE}, - * {@code X.509 CERTIFICATE}, {@code CRL}, and {@code RSA PRIVATE KEY}. + * @implNote This implementation decodes 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 PEMRecord + * @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 * @@ -142,7 +153,7 @@ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public final class PEMDecoder { private final Provider factory; - private final PBEKeySpec password; + private final PBEKeySpec keySpec; // Singleton instance for PEMDecoder private final static PEMDecoder PEM_DECODER = new PEMDecoder(null, null); @@ -154,8 +165,12 @@ public final class PEMDecoder { * decryption */ private PEMDecoder(Provider withFactory, PBEKeySpec withPassword) { - password = withPassword; + keySpec = withPassword; factory = withFactory; + if (withPassword != null) { + final var k = this.keySpec; + CleanerFactory.cleaner().register(this, k::clearPassword); + } } /** @@ -172,7 +187,7 @@ public static PEMDecoder of() { * header and footer and proceed with decoding the base64 for the * appropriate type. */ - private DEREncodable decode(PEMRecord pem) { + private DEREncodable decode(PEM pem) { Base64.Decoder decoder = Base64.getMimeDecoder(); try { @@ -185,41 +200,57 @@ yield getKeyFactory( generatePublic(spec); } case Pem.PRIVATE_KEY -> { - PKCS8Key p8key = new PKCS8Key(decoder.decode(pem.content())); - String algo = p8key.getAlgorithm(); - KeyFactory kf = getKeyFactory(algo); - DEREncodable d = kf.generatePrivate( - new PKCS8EncodedKeySpec(p8key.getEncoded(), algo)); + DEREncodable d; + PKCS8Key p8key = null; + PKCS8EncodedKeySpec p8spec = null; + byte[] encoding = decoder.decode(pem.content()); + + 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); + // 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. This - // happens with EC and SEC1-v2 encoding - X509EncodedKeySpec spec = new X509EncodedKeySpec( - p8.getPubKeyEncoded(), algo); - yield new KeyPair(getKeyFactory(algo). - generatePublic(spec), p8); - } else { - // No public key, return the private key. - yield 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 -> { - if (password == null) { - yield new EncryptedPrivateKeyInfo(decoder.decode( - pem.content())); + byte[] p8 = null; + byte[] encoding = null; + try { + encoding = decoder.decode(pem.content()); + var ekpi = new EncryptedPrivateKeyInfo(encoding); + if (keySpec == null) { + yield ekpi; + } + p8 = Pem.decryptEncoding(ekpi, keySpec); + yield Pem.toDEREncodable(p8, true, factory); + } finally { + Reference.reachabilityFence(this); + KeyUtil.clear(encoding, p8); } - yield new EncryptedPrivateKeyInfo(decoder.decode(pem.content())). - getKey(password.getPassword()); } case Pem.CERTIFICATE, Pem.X509_CERTIFICATE, Pem.X_509_CERTIFICATE -> { @@ -246,28 +277,26 @@ yield new EncryptedPrivateKeyInfo(decoder.decode(pem.content())). } /** - * Decodes and returns a {@link DEREncodable} from the given {@code String}. + * Decodes and returns a {@code DEREncodable} 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. * - *

This method returns a Java API cryptographic object, - * such as a {@code PrivateKey}, if the PEM type is supported. - * Any non-PEM data preceding the PEM header is ignored by the decoder. - * Otherwise, a {@link PEMRecord} will be returned containing - * the type identifier and Base64-encoded data. - * Any non-PEM data preceding the PEM header will be stored in - * {@code leadingData}. + *

A {@code DEREncodable} will be returned that best represents the + * decoded data. 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 DEREncodable} types + * other than {@code PEM}, leading data is ignored and not returned as part + * of the {@code DEREncodable} object. * *

Input consumed by this method is read in as * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. * - * @param str a String containing PEM data + * @param str a {@code String} containing PEM data * @return a {@code DEREncodable} - * @throws IllegalArgumentException on error in decoding or no PEM data - * found - * @throws NullPointerException when {@code str} is null + * @throws IllegalArgumentException on error in decoding or no PEM data found + * @throws NullPointerException when {@code str} is {@code null} */ public DEREncodable decode(String str) { Objects.requireNonNull(str); @@ -281,67 +310,65 @@ public DEREncodable decode(String str) { } /** - * Decodes and returns a {@link DEREncodable} from the given + * Decodes and returns a {@code DEREncodable} from the given * {@code InputStream}. * *

This method reads from the {@code InputStream} until the end of - * the PEM footer or the end of the stream. If an I/O error occurs, + * a PEM footer or the end of the stream. If an I/O error occurs, * the read position in the stream may become inconsistent. * It is recommended to perform no further decoding operations * on the {@code InputStream}. * - *

This method returns a Java API cryptographic object, - * such as a {@code PrivateKey}, if the PEM type is supported. - * Any non-PEM data preceding the PEM header is ignored by the decoder. - * Otherwise, a {@link PEMRecord} will be returned containing - * the type identifier and Base64-encoded data. - * Any non-PEM data preceding the PEM header will be stored in - * {@code leadingData}. + *

A {@code DEREncodable} will be returned that best represents the + * decoded data. 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 DEREncodable} types + * other than {@code PEM}, leading data is ignored and not returned as part + * of the {@code DEREncodable} object. * - *

If no PEM data is found, an {@code IllegalArgumentException} is - * thrown. + *

If no PEM data is found, an {@code EOFException} is thrown. * - * @param is InputStream containing PEM data + * @param is {@code InputStream} containing PEM data * @return a {@code DEREncodable} * @throws IOException on IO or PEM syntax error where the - * {@code InputStream} did not complete decoding. - * @throws EOFException at the end of the {@code InputStream} + * {@code InputStream} did not complete decoding + * @throws EOFException no PEM data found or unexpectedly reached the + * end of the {@code InputStream} * @throws IllegalArgumentException on error in decoding - * @throws NullPointerException when {@code is} is null + * @throws NullPointerException when {@code is} is {@code null} */ public DEREncodable decode(InputStream is) throws IOException { Objects.requireNonNull(is); - PEMRecord pem = Pem.readPEM(is); + PEM pem = Pem.readPEM(is); return decode(pem); } /** * Decodes and returns a {@code DEREncodable} of the specified class from - * the given PEM string. {@code tClass} must extend {@link DEREncodable} - * and be an appropriate class for the PEM type. + * 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 the class parameter is {@code PEMRecord.class}, - * a {@linkplain PEMRecord} is returned containing the - * type identifier and Base64 encoding. Any non-PEM data preceding - * the PEM header will be stored in {@code leadingData}. Other - * class parameters will not return preceding non-PEM data. + *

If the class parameter 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 DEREncodable} types + * other than {@code PEM}, leading data is ignored and not returned as part + * of the {@code DEREncodable} object. * *

Input consumed by this method is read in as * {@link java.nio.charset.StandardCharsets#UTF_8 UTF-8}. * - * @param Class type parameter that extends {@code DEREncodable} - * @param str the String containing PEM data - * @param tClass the returned object class that implements - * {@code DEREncodable} + * @param class type parameter that extends {@code DEREncodable} + * @param str the {@code String} containing PEM data + * @param tClass the returned object class that extends or implements + * {@code DEREncodable} * @return a {@code DEREncodable} specified by {@code tClass} - * @throws IllegalArgumentException on error in decoding or no PEM data - * found - * @throws ClassCastException if {@code tClass} is invalid for the PEM type - * @throws NullPointerException when any input values are null + * @throws IllegalArgumentException on error in decoding or no PEM data found + * @throws ClassCastException if {@code tClass} does not represent the PEM type + * @throws NullPointerException when any input values are {@code null} */ public S decode(String str, Class tClass) { Objects.requireNonNull(str); @@ -355,47 +382,47 @@ public S decode(String str, Class tClass) { } /** - * Decodes and returns the specified class for the given - * {@link InputStream}. The class must extend {@link DEREncodable} and be - * an appropriate class for the PEM type. + * Decodes and returns a {@code DEREncodable} of the specified class for 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 - * the PEM footer or the end of the stream. If an I/O error occurs, + * a PEM footer or the end of the stream. If an I/O error occurs, * the read position in the stream may become inconsistent. * It is recommended to perform no further decoding operations * on the {@code InputStream}. * - *

If the class parameter is {@code PEMRecord.class}, - * a {@linkplain PEMRecord} is returned containing the - * type identifier and Base64 encoding. Any non-PEM data preceding - * the PEM header will be stored in {@code leadingData}. Other - * class parameters will not return preceding non-PEM data. + *

If the class parameter 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 DEREncodable} types + * other than {@code PEM}, leading data is ignored and not returned as part + * of the {@code DEREncodable} object. * - *

If no PEM data is found, an {@code IllegalArgumentException} is - * thrown. + *

If no PEM data is found, an {@code EOFException} is thrown. * - * @param Class type parameter that extends {@code DEREncodable}. - * @param is an InputStream containing PEM data - * @param tClass the returned object class that implements - * {@code DEREncodable}. + * @param class type parameter that extends {@code DEREncodable} + * @param is an {@code InputStream} containing PEM data + * @param tClass the returned object class that extends or implements + * {@code DEREncodable} * @return a {@code DEREncodable} typecast to {@code tClass} * @throws IOException on IO or PEM syntax error where the - * {@code InputStream} did not complete decoding. - * @throws EOFException at the end of the {@code InputStream} + * {@code InputStream} did not complete decoding + * @throws EOFException no PEM data found or unexpectedly reached the + * end of the {@code InputStream} * @throws IllegalArgumentException on error in decoding - * @throws ClassCastException if {@code tClass} is invalid for the PEM type - * @throws NullPointerException when any input values are null + * @throws ClassCastException if {@code tClass} does not represent the PEM type + * @throws NullPointerException when any input values are {@code null} * - * @see #decode(InputStream) + * @see #decode(InputStream) * @see #decode(String, Class) */ public S decode(InputStream is, Class tClass) throws IOException { Objects.requireNonNull(is); Objects.requireNonNull(tClass); - PEMRecord pem = Pem.readPEM(is); + PEM pem = Pem.readPEM(is); - if (tClass.isAssignableFrom(PEMRecord.class)) { + if (tClass.isAssignableFrom(PEM.class)) { return tClass.cast(pem); } DEREncodable so = decode(pem); @@ -478,28 +505,28 @@ private CertificateFactory getCertFactory(String algorithm) { /** * Returns a copy of this {@code PEMDecoder} instance that uses - * {@link KeyFactory} and {@link CertificateFactory} implementations - * from the specified {@link Provider} to produce cryptographic objects. + * {@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 provider - * @return a new PEMEncoder instance configured to the {@code Provider}. - * @throws NullPointerException if {@code provider} is null + * @return a new {@code PEMDecoder} instance configured with the {@code Provider} + * @throws NullPointerException if {@code provider} is {@code null} */ public PEMDecoder withFactory(Provider provider) { Objects.requireNonNull(provider); - return new PEMDecoder(provider, password); + return new PEMDecoder(provider, keySpec); } /** * Returns a copy of this {@code PEMDecoder} that decodes and decrypts * encrypted private keys using the specified password. - * Non-encrypted PEM can still be decoded from this instance. + * Non-encrypted PEM can also be decoded from this instance. * - * @param password the password to decrypt encrypted PEM data. This array + * @param password the password to decrypt the encrypted PEM data. This array * is cloned and stored in the new instance. - * @return a new PEMEncoder instance configured for decryption - * @throws NullPointerException if {@code password} is null + * @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); diff --git a/src/java.base/share/classes/java/security/PEMEncoder.java b/src/java.base/share/classes/java/security/PEMEncoder.java index 95fcffe967b74..62a0942caf779 100644 --- a/src/java.base/share/classes/java/security/PEMEncoder.java +++ b/src/java.base/share/classes/java/security/PEMEncoder.java @@ -27,10 +27,8 @@ import jdk.internal.javac.PreviewFeature; import sun.security.pkcs.PKCS8Key; -import sun.security.util.DerOutputStream; -import sun.security.util.DerValue; +import sun.security.util.KeyUtil; import sun.security.util.Pem; -import sun.security.x509.AlgorithmId; import javax.crypto.*; import javax.crypto.spec.PBEKeySpec; @@ -41,83 +39,84 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Objects; -import java.util.concurrent.locks.ReentrantLock; /** * {@code PEMEncoder} implements an encoder for Privacy-Enhanced Mail (PEM) - * data. PEM is a textual encoding used to store and transfer security + * data. PEM is a textual encoding used to store and transfer cryptographic * objects, such as asymmetric keys, certificates, and certificate revocation - * lists (CRL). It is defined in RFC 1421 and RFC 7468. PEM consists of a - * Base64-formatted binary encoding enclosed by a type-identifying header + * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of a + * Base64-encoded binary encoding enclosed by a type-identifying header * and footer. * - *

Encoding may be performed on Java API cryptographic objects that + *

Encoding can be performed on cryptographic objects that * implement {@link DEREncodable}. The {@link #encode(DEREncodable)} - * and {@link #encodeToString(DEREncodable)} methods encode a DEREncodable - * into PEM and return the data in a byte array or String. + * and {@link #encodeToString(DEREncodable)} methods encode a {@code DEREncodable} + * into PEM and return the data in a byte array or {@code String}. * *

Private keys can be encrypted and encoded by configuring a - * {@code PEMEncoder} with the {@linkplain #withEncryption(char[])} method, + * {@code PEMEncoder} with the {@link #withEncryption(char[])} method, * which takes a password and returns a new {@code PEMEncoder} instance * configured to encrypt the key with that password. Alternatively, a - * private key encrypted as an {@code EncryptedKeyInfo} object can be encoded + * private key encrypted as an {@link EncryptedPrivateKeyInfo} object can be encoded * directly to PEM by passing it to the {@code encode} or * {@code encodeToString} methods. * - *

PKCS #8 2.0 defines the ASN.1 OneAsymmetricKey structure, which may + *

PKCS #8 v2.0 defines the ASN.1 OneAsymmetricKey structure, which may * contain both private and public keys. - * {@link KeyPair} objects passed to the {@code encode} or + * {@code KeyPair} objects passed to the {@code encode} or * {@code encodeToString} methods are encoded as a * OneAsymmetricKey structure using the "PRIVATE KEY" type. * - *

When encoding a {@link PEMRecord}, the API surrounds the - * {@linkplain PEMRecord#content()} with the PEM header and footer - * from {@linkplain PEMRecord#type()}. {@linkplain PEMRecord#leadingData()} is - * not included in the encoding. {@code PEMRecord} will not perform - * validity checks on the data. - * - *

The following lists the supported {@code DEREncodable} classes and - * the PEM types that each are encoded as: + *

When encoding a {@link PEM} object, the API surrounds + * {@link PEM#content()} with a PEM header and footer based on + * {@link PEM#type()}. The value returned by {@link PEM#leadingData()} is not + * included in the output. * + *

The following lists the supported {@code DEREncodable} classes and + * the PEM types they encode as: + *

+ *

When used with a {@code PEMEncoder} instance configured for encryption: *

    - *
  • {@code X509Certificate} : CERTIFICATE
  • - *
  • {@code X509CRL} : X509 CRL
  • - *
  • {@code PublicKey}: PUBLIC KEY
  • - *
  • {@code PrivateKey} : PRIVATE KEY
  • - *
  • {@code PrivateKey} (if configured with encryption): - * ENCRYPTED PRIVATE KEY
  • - *
  • {@code EncryptedPrivateKeyInfo} : ENCRYPTED PRIVATE KEY
  • - *
  • {@code KeyPair} : PRIVATE KEY
  • - *
  • {@code X509EncodedKeySpec} : PUBLIC KEY
  • - *
  • {@code PKCS8EncodedKeySpec} : PRIVATE KEY
  • - *
  • {@code PEMRecord} : {@code PEMRecord.type()}
  • - *
+ *
  • {@link PrivateKey} : ENCRYPTED PRIVATE KEY
  • + *
  • {@link KeyPair} : ENCRYPTED PRIVATE KEY
  • + *
  • {@link PKCS8EncodedKeySpec} : ENCRYPTED PRIVATE KEY
  • + * * *

    This class is immutable and thread-safe. * - *

    Here is an example of encoding a {@code PrivateKey} object: + *

    Example: encode a private key: * {@snippet lang = java: * PEMEncoder pe = PEMEncoder.of(); * byte[] pemData = pe.encode(privKey); * } * - *

    Here is an example that encrypts and encodes a private key using the - * specified password: + *

    Example: encrypt and encode a private key using a password: * {@snippet lang = java: * PEMEncoder pe = PEMEncoder.of().withEncryption(password); * byte[] pemData = pe.encode(privKey); * } * - * @implNote An implementation may support other PEM types and - * {@code DEREncodable} objects. + * @implNote Implementations may support additional PEM types. * * * @see PEMDecoder - * @see PEMRecord + * @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 * @@ -128,24 +127,26 @@ public final class PEMEncoder { // Singleton instance of PEMEncoder private static final PEMEncoder PEM_ENCODER = new PEMEncoder(null); - - // Stores the password for an encrypted encoder that isn't setup yet. - private PBEKeySpec keySpec; - // Stores the key after the encoder is ready to encrypt. The prevents - // repeated SecretKeyFactory calls if the encoder is used on multiple keys. - private SecretKey key; - // Makes SecretKeyFactory generation thread-safe. - private final ReentrantLock lock; + // PBE key for encryption + private final Key key; /** - * Instantiate a {@code PEMEncoder} for Encrypted Private Keys. - * - * @param pbe contains the password spec used for encryption. + * Create an encrypted {@code PEMEncoder} instance. */ - private PEMEncoder(PBEKeySpec pbe) { - keySpec = pbe; - key = null; - lock = new ReentrantLock(); + private PEMEncoder(PBEKeySpec keySpec) { + if (keySpec != null) { + try { + key = SecretKeyFactory.getInstance(Pem.DEFAULT_ALGO). + generateSecret(keySpec); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Operation failed: " + + "unable to generate key or locate a valid algorithm. " + + "Check the jdk.epkcs8.defaultAlgorithm security " + + "property for a valid configuration.", e); + } + } else { + key = null; + } } /** @@ -158,70 +159,90 @@ public static PEMEncoder of() { } /** - * Encodes the specified {@code DEREncodable} and returns a PEM encoded + * Encodes the specified {@code DEREncodable} and returns a PEM-encoded * string. * * @param de the {@code DEREncodable} to be encoded - * @return a {@code String} containing the PEM encoded data - * @throws IllegalArgumentException if the {@code DEREncodable} cannot be - * encoded + * @return a {@code String} containing the PEM-encoded data + * @throws IllegalArgumentException if the {@code DEREncodable} cannot be encoded * @throws NullPointerException if {@code de} is {@code null} * @see #withEncryption(char[]) */ public String encodeToString(DEREncodable de) { Objects.requireNonNull(de); return switch (de) { - case PublicKey pu -> buildKey(null, pu.getEncoded()); - case PrivateKey pr -> buildKey(pr.getEncoded(), null); - case KeyPair kp -> { - if (kp.getPublic() == null) { - throw new IllegalArgumentException("KeyPair does not " + - "contain PublicKey."); + case PublicKey pu -> buildKey(pu.getEncoded(), null); + case PrivateKey pr -> { + byte[] encoding = pr.getEncoded(); + try { + yield buildKey(null, encoding); + } finally { + KeyUtil.clear(encoding); } - if (kp.getPrivate() == null) { - throw new IllegalArgumentException("KeyPair does not " + - "contain PrivateKey."); + } + case KeyPair kp -> { + byte[] encoding = null; + try { + if (kp.getPublic() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PublicKey."); + } + if (kp.getPrivate() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PrivateKey."); + } + encoding = kp.getPrivate().getEncoded(); + if (encoding == null || encoding.length == 0) { + throw new IllegalArgumentException("PrivateKey is " + + "null or has no encoding."); + } + yield buildKey(kp.getPublic().getEncoded(), encoding); + } finally { + KeyUtil.clear(encoding); } - yield buildKey(kp.getPrivate().getEncoded(), - kp.getPublic().getEncoded()); } - case X509EncodedKeySpec x -> - buildKey(null, x.getEncoded()); - case PKCS8EncodedKeySpec p -> - buildKey(p.getEncoded(), null); + case X509EncodedKeySpec x -> buildKey(x.getEncoded(), null); + case PKCS8EncodedKeySpec p -> buildKey(null, p.getEncoded()); case EncryptedPrivateKeyInfo epki -> { + byte[] encoding = null; + if (key != null) { + throw new IllegalArgumentException( + "EncryptedPrivateKeyInfo cannot be encrypted"); + } try { - yield Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, - epki.getEncoded()); + encoding = epki.getEncoded(); + yield Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, encoding); } catch (IOException e) { throw new IllegalArgumentException(e); + } finally { + KeyUtil.clear(encoding); } } case X509Certificate c -> { + if (key != null) { + throw new IllegalArgumentException("Certificates " + + "cannot be encrypted"); + } try { - if (isEncrypted()) { - throw new IllegalArgumentException("Certificates " + - "cannot be encrypted"); - } yield Pem.pemEncoded(Pem.CERTIFICATE, c.getEncoded()); } catch (CertificateEncodingException e) { throw new IllegalArgumentException(e); } } case X509CRL crl -> { + if (key != null) { + throw new IllegalArgumentException("CRLs cannot be " + + "encrypted"); + } try { - if (isEncrypted()) { - throw new IllegalArgumentException("CRLs cannot be " + - "encrypted"); - } yield Pem.pemEncoded(Pem.X509_CRL, crl.getEncoded()); } catch (CRLException e) { throw new IllegalArgumentException(e); } } - case PEMRecord rec -> { - if (isEncrypted()) { - throw new IllegalArgumentException("PEMRecord cannot be " + + case PEM rec -> { + if (key != null) { + throw new IllegalArgumentException("PEM cannot be " + "encrypted"); } yield Pem.pemEncoded(rec); @@ -233,13 +254,12 @@ yield buildKey(kp.getPrivate().getEncoded(), } /** - * Encodes the specified {@code DEREncodable} and returns the PEM encoding - * in a byte array. + * Encodes the specified {@code DEREncodable} and returns a PEM-encoded + * byte array. * * @param de the {@code DEREncodable} to be encoded - * @return a PEM encoded byte array - * @throws IllegalArgumentException if the {@code DEREncodable} cannot be - * encoded + * @return a PEM-encoded byte array + * @throws IllegalArgumentException if the {@code DEREncodable} cannot be encoded * @throws NullPointerException if {@code de} is {@code null} * @see #withEncryption(char[]) */ @@ -248,136 +268,95 @@ public byte[] encode(DEREncodable de) { } /** - * Returns a new {@code PEMEncoder} instance configured for encryption - * with the default algorithm and a given password. + * Returns a copy of this PEMEncoder that encrypts and encodes + * using the specified password and default encryption algorithm. * - *

    Only {@link PrivateKey} objects can be encrypted with this newly - * configured instance. Encoding other {@link DEREncodable} objects will + *

    Only {@code PrivateKey}, {@code KeyPair}, and + * {@code PKCS8EncodedKeySpec} objects can be encoded with this newly + * configured instance. Encoding other {@code DEREncodable} objects will * throw an {@code IllegalArgumentException}. * - * @implNote - * The default password-based encryption algorithm is defined - * by the {@code jdk.epkcs8.defaultAlgorithm} security property and - * uses the default encryption parameters of the provider that is selected. - * For greater flexibility with encryption options and parameters, use - * {@link EncryptedPrivateKeyInfo#encryptKey(PrivateKey, Key, + * @implNote The {@code jdk.epkcs8.defaultAlgorithm} security property + * defines the default encryption algorithm. The {@code AlgorithmParameterSpec} + * defaults are determined by the provider. To use non-default encryption + * parameters, or to encrypt with a different encryption provider, use + * {@link EncryptedPrivateKeyInfo#encrypt(DEREncodable, Key, * String, AlgorithmParameterSpec, Provider, SecureRandom)} and use the * returned object with {@link #encode(DEREncodable)}. * * @param password the encryption password. The array is cloned and - * stored in the new instance. + * stored in the new instance. * @return a new {@code PEMEncoder} instance configured for encryption - * @throws NullPointerException when password is {@code null} + * @throws NullPointerException if password is {@code null} + * @throws IllegalArgumentException if generating the encryption key fails */ public PEMEncoder withEncryption(char[] password) { - // PBEKeySpec clones the password Objects.requireNonNull(password, "password cannot be null."); - return new PEMEncoder(new PBEKeySpec(password)); + PBEKeySpec keySpec = new PBEKeySpec(password); + try { + return new PEMEncoder(keySpec); + } finally { + keySpec.clearPassword(); + } } /** * Build PEM encoding. + * + * privateKeyEncoding will be zeroed when the method returns */ - private String buildKey(byte[] privateBytes, byte[] publicBytes) { - DerOutputStream out = new DerOutputStream(); - Cipher cipher; - - if (privateBytes == null && publicBytes == null) { + private String buildKey(byte[] publicEncoding, byte[] privateEncoding) { + if (publicEncoding == null && privateEncoding == null) { throw new IllegalArgumentException("No encoded data given by the " + "DEREncodable."); } - // If `keySpec` is non-null, then `key` hasn't been established. - // Setting a `key` prevents repeated key generation operations. - // withEncryption() is a configuration method and cannot throw an - // exception; therefore generation is delayed. - if (keySpec != null) { - // For thread safety - lock.lock(); - if (key == null) { - try { - key = SecretKeyFactory.getInstance(Pem.DEFAULT_ALGO). - generateSecret(keySpec); - keySpec.clearPassword(); - keySpec = null; - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("Security property " + - "\"jdk.epkcs8.defaultAlgorithm\" may not specify a " + - "valid algorithm. Operation cannot be performed.", e); - } finally { - lock.unlock(); - } - } else { - lock.unlock(); - } + if (publicEncoding != null && publicEncoding.length == 0) { + throw new IllegalArgumentException("Public key has no " + + "encoding"); } - // If `key` is non-null, this is an encoder ready to encrypt. - if (key != null) { - if (privateBytes == null || publicBytes != null) { - throw new IllegalArgumentException("Can only encrypt a " + - "PrivateKey."); - } - - try { - cipher = Cipher.getInstance(Pem.DEFAULT_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, key); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("Security property " + - "\"jdk.epkcs8.defaultAlgorithm\" may not specify a " + - "valid algorithm. Operation cannot be performed.", e); - } + if (privateEncoding != null && privateEncoding.length == 0) { + throw new IllegalArgumentException("Private key has no " + + "encoding"); + } - try { - new AlgorithmId(Pem.getPBEID(Pem.DEFAULT_ALGO), - cipher.getParameters()).encode(out); - out.putOctetString(cipher.doFinal(privateBytes)); - return Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, - DerValue.wrap(DerValue.tag_Sequence, out).toByteArray()); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException(e); - } + if (key != null && privateEncoding == null) { + throw new IllegalArgumentException("This DEREncodable cannot " + + "be encrypted."); } // X509 only - if (publicBytes != null && privateBytes == null) { - if (publicBytes.length == 0) { - throw new IllegalArgumentException("No public key encoding " + - "given by the DEREncodable."); - } - - return Pem.pemEncoded(Pem.PUBLIC_KEY, publicBytes); + if (publicEncoding != null && privateEncoding == null) { + return Pem.pemEncoded(Pem.PUBLIC_KEY, publicEncoding); } - // PKCS8 only - if (publicBytes == null && privateBytes != null) { - if (privateBytes.length == 0) { + byte[] encoding = null; + PKCS8EncodedKeySpec p8KeySpec = null; + try { + if (publicEncoding == null) { + encoding = privateEncoding; + } else { + encoding = PKCS8Key.getEncoded(publicEncoding, + privateEncoding); + } + if (key != null) { + p8KeySpec = new PKCS8EncodedKeySpec(encoding); + encoding = EncryptedPrivateKeyInfo.encrypt(p8KeySpec, key, + Pem.DEFAULT_ALGO, null, null, null). + getEncoded(); + } + if (encoding.length == 0) { throw new IllegalArgumentException("No private key encoding " + "given by the DEREncodable."); } - - return Pem.pemEncoded(Pem.PRIVATE_KEY, privateBytes); - } - - // OneAsymmetricKey - if (privateBytes.length == 0) { - throw new IllegalArgumentException("No private key encoding " + - "given by the DEREncodable."); - } - - if (publicBytes.length == 0) { - throw new IllegalArgumentException("No public key encoding " + - "given by the DEREncodable."); - } - try { - return Pem.pemEncoded(Pem.PRIVATE_KEY, - PKCS8Key.getEncoded(publicBytes, privateBytes)); + return Pem.pemEncoded( + (key == null ? Pem.PRIVATE_KEY : Pem.ENCRYPTED_PRIVATE_KEY), + encoding); } catch (IOException e) { - throw new IllegalArgumentException(e); + throw new IllegalArgumentException("Error while encoding", e); + } finally { + KeyUtil.clear(encoding, p8KeySpec); } } - - private boolean isEncrypted() { - return (key != null || keySpec != null); - } } diff --git a/src/java.base/share/classes/java/security/PEMRecord.java b/src/java.base/share/classes/java/security/PEMRecord.java deleted file mode 100644 index 2ce567f9fdede..0000000000000 --- a/src/java.base/share/classes/java/security/PEMRecord.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 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 java.security; - -import jdk.internal.javac.PreviewFeature; - -import sun.security.util.Pem; - -import java.util.Objects; - -/** - * {@code PEMRecord} is a {@link DEREncodable} that represents Privacy-Enhanced - * Mail (PEM) data by its type and Base64 form. {@link PEMDecoder} and - * {@link PEMEncoder} use {@code PEMRecord} when representing the data as a - * cryptographic object is not desired or the type has no - * {@code DEREncodable}. - * - *

    {@code type} and {@code content} may not be {@code null}. - * {@code leadingData} may be null if no non-PEM data preceded PEM header - * during decoding. {@code leadingData} may be useful for reading metadata - * that accompanies PEM data. - * - *

    No validation is performed during instantiation to ensure that - * {@code type} conforms to {@code RFC 7468}, that {@code content} is valid - * Base64, or that {@code content} matches the {@code type}. - * {@code leadingData} is not defensively copied and does not return a - * clone when {@linkplain #leadingData()} is called. - * - * @param type the type identifier in the PEM header without PEM syntax labels. - * For a public key, {@code type} would be "PUBLIC KEY". - * @param content the Base64-encoded data, excluding the PEM header and footer - * @param leadingData any non-PEM data preceding the PEM header when decoding. - * - * @spec https://www.rfc-editor.org/info/rfc7468 - * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures - * - * @see PEMDecoder - * @see PEMEncoder - * - * @since 25 - */ -@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) -public record PEMRecord(String type, String content, byte[] leadingData) - implements DEREncodable { - - /** - * Creates a {@code PEMRecord} instance with the given parameters. - * - * @param type the type identifier - * @param content the Base64-encoded data, excluding the PEM header and - * footer - * @param leadingData any non-PEM data read during the decoding process - * before the PEM header. This value maybe {@code null}. - * @throws IllegalArgumentException if {@code type} is incorrectly - * formatted. - * @throws NullPointerException if {@code type} and/or {@code content} are - * {@code null}. - */ - public PEMRecord { - Objects.requireNonNull(type, "\"type\" cannot be null."); - Objects.requireNonNull(content, "\"content\" cannot be null."); - - // With no validity checking on `type`, the constructor accept anything - // including lowercase. The onus is on the caller. - if (type.startsWith("-") || type.startsWith("BEGIN ") || - type.startsWith("END ")) { - throw new IllegalArgumentException("PEM syntax labels found. " + - "Only the PEM type identifier is allowed"); - } - - } - - /** - * Creates a {@code PEMRecord} instance with a given {@code type} and - * {@code content} data in String form. {@code leadingData} is set to null. - * - * @param type the PEM type identifier - * @param content the Base64-encoded data, excluding the PEM header and - * footer - * @throws IllegalArgumentException if {@code type} is incorrectly - * formatted. - * @throws NullPointerException if {@code type} and/or {@code content} are - * {@code null}. - */ - public PEMRecord(String type, String content) { - this(type, content, null); - } - - /** - * Returns the type and Base64 encoding in PEM format. {@code leadingData} - * is not returned by this method. - */ - @Override - public String toString() { - return Pem.pemEncoded(this); - } -} diff --git a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java index 90316d7437e04..15933183e5f2d 100644 --- a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java +++ b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java @@ -276,8 +276,7 @@ public byte[] getEncryptedData() { * @param cipher the initialized {@code Cipher} object which will be * used for decrypting the encrypted data. * @return the PKCS8EncodedKeySpec object. - * @exception NullPointerException if {@code cipher} - * is {@code null}. + * @exception NullPointerException if {@code cipher} is {@code null}. * @exception InvalidKeySpecException if the given cipher is * inappropriate for the encrypted data or the encrypted * data is corrupted and cannot be decrypted. @@ -296,10 +295,9 @@ public PKCS8EncodedKeySpec getKeySpec(Cipher cipher) } } - private PKCS8EncodedKeySpec getKeySpecImpl(Key decryptKey, - Provider provider) throws NoSuchAlgorithmException, - InvalidKeyException { - byte[] encoded; + // Return the decrypted encryptedData in this instance. + private byte[] decryptData(Key decryptKey, Provider provider) + throws NoSuchAlgorithmException, InvalidKeyException { Cipher c; try { if (provider == null) { @@ -308,162 +306,157 @@ private PKCS8EncodedKeySpec getKeySpecImpl(Key decryptKey, } else { c = Cipher.getInstance(getAlgName(), provider); } + } catch (NoSuchPaddingException e) { + throw new NoSuchAlgorithmException(e); + } + try { c.init(Cipher.DECRYPT_MODE, decryptKey, getAlgParameters()); - encoded = c.doFinal(encryptedData); - return pkcs8EncodingToSpec(encoded); + return c.doFinal(encryptedData); + } catch (GeneralSecurityException e) { + throw new InvalidKeyException(e); + } + } + + // Wrap the decrypted encryptedData in a P8EKS for getKeySpec methods. + private PKCS8EncodedKeySpec getKeySpecImpl(Key decryptKey, + Provider provider) throws NoSuchAlgorithmException, + InvalidKeyException { + byte[] encoding = null; + try { + encoding = decryptData(decryptKey, provider); + return pkcs8EncodingToSpec(encoding); } catch (NoSuchAlgorithmException nsae) { // rethrow throw nsae; } catch (GeneralSecurityException | IOException ex) { throw new InvalidKeyException( - "Cannot retrieve the PKCS8EncodedKeySpec", ex); + "Cannot retrieve the PKCS8EncodedKeySpec", ex); + } finally { + KeyUtil.clear(encoding); } } /** - * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from a given - * {@code PrivateKey}. A valid password-based encryption (PBE) algorithm + * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified + * {@code DEREncodable}. A valid password-based encryption (PBE) algorithm * and password must be specified. * - *

    The PBE algorithm string format details can be found in the + *

    The format of the PBE algorithm string is described in the * - * Cipher section of the Java Security Standard Algorithm Names + * Cipher Algorithms section of the Java Security Standard Algorithm Names * Specification. * - * @param key the {@code PrivateKey} to be encrypted - * @param password the password used in the PBE encryption. This array - * will be cloned before being used. - * @param algorithm the PBE encryption algorithm. The default algorithm - * will be used if {@code null}. However, {@code null} is - * not allowed when {@code params} is non-null. - * @param params the {@code AlgorithmParameterSpec} to be used with - * encryption. The provider default will be used if - * {@code null}. - * @param provider the {@code Provider} will be used for PBE - * {@link SecretKeyFactory} generation and {@link Cipher} - * encryption operations. The default provider list will be - * used if {@code null}. + * @param de the {@code DEREncodable} to encrypt. Supported types include + * {@code PrivateKey}, {@code KeyPair}, and {@code PKCS8EncodedKeySpec}. + * @param password the password used for PBE encryption. This array is cloned + * before use. + * @param algorithm the PBE encryption algorithm + * @param params the {@code AlgorithmParameterSpec} used for encryption. If + * {@code null}, the provider’s default parameters are applied. + * @param provider the {@code Provider} for {@code SecretKeyFactory} and + * {@code Cipher} operations. If {@code null}, provider + * defaults are used. * @return an {@code EncryptedPrivateKeyInfo} - * @throws IllegalArgumentException on initialization errors based on the - * arguments passed to the method - * @throws RuntimeException on an encryption error - * @throws NullPointerException if the key or password are {@code null}. If - * {@code params} is non-null when {@code algorithm} is {@code null}. - * - * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property - * defines the default encryption algorithm and the - * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * @throws NullPointerException if {@code de}, {@code password}, or + * {@code algorithm} is {@code null} + * @throws IllegalArgumentException if {@code de} is an unsupported + * {@code DEREncodable}, if an error occurs while generating the + * PBE key, if {@code algorithm} or {@code params} are + * not supported by any provider, or if an error occurs during + * encryption. * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, + public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, char[] password, String algorithm, AlgorithmParameterSpec params, Provider provider) { - SecretKey skey; - Objects.requireNonNull(key, "key cannot be null"); - Objects.requireNonNull(password, "password cannot be null."); - PBEKeySpec keySpec = new PBEKeySpec(password); - if (algorithm == null) { - if (params != null) { - throw new NullPointerException("algorithm must be specified" + - " if params is non-null."); - } - algorithm = Pem.DEFAULT_ALGO; - } - + Objects.requireNonNull(de, "a key must be specified."); + Objects.requireNonNull(password, "a password must be specified."); + Objects.requireNonNull(algorithm, "an algorithm must be specified."); + char[] passwd = password.clone(); + byte[] encoding = getEncoding(de); try { - SecretKeyFactory factory; - if (provider == null) { - factory = SecretKeyFactory.getInstance(algorithm); - } else { - factory = SecretKeyFactory.getInstance(algorithm, provider); - } - skey = factory.generateSecret(keySpec); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalArgumentException(e); + return encryptImpl(encoding, algorithm, + generateSecretKey(passwd, algorithm, provider), params, + provider, null); + } finally { + KeyUtil.clear(passwd, encoding); } - return encryptKeyImpl(key, algorithm, skey, params, provider, null); } - /** - * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from a given - * {@code PrivateKey} and password. Default algorithm and parameters are - * used. + * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified + * {@code DEREncodable}. A valid password must be specified. A default + * password-based encryption (PBE) algorithm and provider are used. * - * @param key the {@code PrivateKey} to be encrypted - * @param password the password used in the PBE encryption. This array - * will be cloned before being used. + * @param de the {@code DEREncodable} to encrypt. Supported types include + * {@code PrivateKey}, {@code KeyPair}, and {@code PKCS8EncodedKeySpec}. + * @param password the password used for PBE encryption. This array is cloned + * before use. * @return an {@code EncryptedPrivateKeyInfo} - * @throws IllegalArgumentException on initialization errors based on the - * arguments passed to the method - * @throws RuntimeException on an encryption error - * @throws NullPointerException when the {@code key} or {@code password} - * is {@code null} + * @throws NullPointerException if {@code de} or {@code password} is {@code null} + * @throws IllegalArgumentException if {@code de} is an unsupported + * {@code DEREncodable}, if an error occurs while generating the + * PBE key, or if the default algorithm is misconfigured * - * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property - * defines the default encryption algorithm and the - * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * @implNote The {@code jdk.epkcs8.defaultAlgorithm} security property + * defines the default encryption algorithm. The {@code AlgorithmParameterSpec} + * defaults are determined by the provider. * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, + public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, char[] password) { - return encryptKey(key, password, Pem.DEFAULT_ALGO, null, null); + return encrypt(de, password, Pem.DEFAULT_ALGO, null, + null); } /** - * Creates and encrypts an {@code EncryptedPrivateKeyInfo} from the given - * {@link PrivateKey} using the {@code encKey} and given parameters. + * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified + * {@code DEREncodable}. A valid encryption algorithm and {@code Key} must + * be specified. * - * @param key the {@code PrivateKey} to be encrypted - * @param encKey the password-based encryption (PBE) {@code Key} used to - * encrypt {@code key}. - * @param algorithm the PBE encryption algorithm. The default algorithm is - * will be used if {@code null}; however, {@code null} is - * not allowed when {@code params} is non-null. - * @param params the {@code AlgorithmParameterSpec} to be used with - * encryption. The provider list default will be used if - * {@code null}. - * @param random the {@code SecureRandom} instance used during - * encryption. The default will be used if {@code null}. - * @param provider the {@code Provider} is used for {@link Cipher} - * encryption operation. The default provider list will be - * used if {@code null}. - * @return an {@code EncryptedPrivateKeyInfo} - * @throws IllegalArgumentException on initialization errors based on the - * arguments passed to the method - * @throws RuntimeException on an encryption error - * @throws NullPointerException if the {@code key} or {@code encKey} are - * {@code null}. If {@code params} is non-null, {@code algorithm} cannot be - * {@code null}. + *

    The format of the algorithm string is described in the + * + * Cipher Algorithms section of the Java Security Standard Algorithm Names + * Specification. * - * @implNote The {@code jdk.epkcs8.defaultAlgorithm} Security Property - * defines the default encryption algorithm and the - * {@code AlgorithmParameterSpec} are the provider's algorithm defaults. + * @param de the {@code DEREncodable} to encrypt. Supported types include + * {@code PrivateKey}, {@code KeyPair}, and {@code PKCS8EncodedKeySpec}. + * @param encryptKey the key used to encrypt the encoding + * @param algorithm the encryption algorithm, such as a password-based + * encryption (PBE) algorithm + * @param params the {@code AlgorithmParameterSpec} used for encryption. If + * {@code null}, the provider’s default parameters are applied. + * @param random the {@code SecureRandom} instance used during encryption. + * If {@code null}, the default is used. + * @param provider the {@code Provider} for {@code Cipher} operations. + * If {@code null}, the default provider list is used. + * @return an {@code EncryptedPrivateKeyInfo} + * @throws NullPointerException if {@code de}, {@code encryptKey}, or + * {@code algorithm} is {@code null} + * @throws IllegalArgumentException if {@code de} is an unsupported + * {@code DEREncodable}, if {@code encryptKey} is invalid, if + * {@code algorithm} or {@code params} are not supported by any + * provider, or if an error occurs during encryption * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encryptKey(PrivateKey key, Key encKey, - String algorithm, AlgorithmParameterSpec params, Provider provider, - SecureRandom random) { - - Objects.requireNonNull(key); - Objects.requireNonNull(encKey); - if (algorithm == null) { - if (params != null) { - throw new NullPointerException("algorithm must be specified " + - "if params is non-null."); - } - algorithm = Pem.DEFAULT_ALGO; - } - return encryptKeyImpl(key, algorithm, encKey, params, provider, random); + public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, + Key encryptKey, String algorithm, AlgorithmParameterSpec params, + Provider provider, SecureRandom random) { + + Objects.requireNonNull(de, "a key must be specified."); + Objects.requireNonNull(encryptKey, "an encryption key must be specified."); + Objects.requireNonNull(algorithm, "an algorithm must be specified."); + return encryptImpl(getEncoding(de), algorithm, encryptKey, + params, provider, random); } - private static EncryptedPrivateKeyInfo encryptKeyImpl(PrivateKey key, + private static EncryptedPrivateKeyInfo encryptImpl(byte[] encoded, String algorithm, Key encryptKey, AlgorithmParameterSpec params, Provider provider, SecureRandom random) { AlgorithmId algId; @@ -481,17 +474,26 @@ private static EncryptedPrivateKeyInfo encryptKeyImpl(PrivateKey key, c = Cipher.getInstance(algorithm, provider); } c.init(Cipher.ENCRYPT_MODE, encryptKey, params, random); - encryptedData = c.doFinal(key.getEncoded()); - algId = new AlgorithmId(Pem.getPBEID(algorithm), c.getParameters()); + encryptedData = c.doFinal(encoded); + try { + // Use shared PEM method for very likely case the algorithm is PBE. + algId = new AlgorithmId(Pem.getPBEID(algorithm), c.getParameters()); + } catch (IllegalArgumentException e) { + // For the unlikely case a non-PBE cipher is used, get the OID. + algId = new AlgorithmId(AlgorithmId.get(algorithm).getOID(), + c.getParameters()); + } out = new DerOutputStream(); algId.encode(out); out.putOctetString(encryptedData); } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | - NoSuchPaddingException e) { + IllegalStateException | NoSuchPaddingException | + IllegalBlockSizeException | InvalidKeyException e) { throw new IllegalArgumentException(e); - } catch (IllegalBlockSizeException | BadPaddingException | - InvalidKeyException e) { - throw new RuntimeException(e); + } catch (BadPaddingException e) { + throw new AssertionError(e); + } finally { + KeyUtil.clear(encoded); } return new EncryptedPrivateKeyInfo( DerValue.wrap(DerValue.tag_Sequence, out).toByteArray(), @@ -499,63 +501,129 @@ private static EncryptedPrivateKeyInfo encryptKeyImpl(PrivateKey key, } /** - * Extract the enclosed {@code PrivateKey} object from the encrypted data - * and return it. + * Extracts and returns the enclosed {@code PrivateKey} using the + * specified password. * - * @param password the password used in the PBE encryption. This array - * will be cloned before being used. - * @return a {@code PrivateKey} - * @throws GeneralSecurityException if an error occurs parsing or - * decrypting the encrypted data, or producing the key object. - * @throws NullPointerException if {@code password} is null + * @param password the password used for PBE decryption. The array is cloned + * before use. + * @return the decrypted {@code PrivateKey} + * @throws NullPointerException if {@code password} is {@code null} + * @throws NoSuchAlgorithmException if the decryption algorithm is unsupported + * @throws InvalidKeyException if an error occurs during parsing, + * decryption, or key generation * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public PrivateKey getKey(char[] password) throws GeneralSecurityException { - SecretKeyFactory skf; - PKCS8EncodedKeySpec p8KeySpec; - Objects.requireNonNull(password, "password cannot be null"); + public PrivateKey getKey(char[] password) + throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(password, "a password must be specified."); PBEKeySpec keySpec = new PBEKeySpec(password); - skf = SecretKeyFactory.getInstance(getAlgName()); - p8KeySpec = getKeySpec(skf.generateSecret(keySpec)); - - return PKCS8Key.parseKey(p8KeySpec.getEncoded()); + try { + return PKCS8Key.parseKey(Pem.decryptEncoding(this, keySpec), null); + } finally { + keySpec.clearPassword(); + } } /** - * Extract the enclosed {@code PrivateKey} object from the encrypted data - * and return it. + * Extracts and returns the enclosed {@code PrivateKey} using the specified + * decryption key and provider. * - * @param decryptKey the decryption key and cannot be {@code null} - * @param provider the {@code Provider} used for Cipher decryption and - * {@code PrivateKey} generation. A {@code null} value will - * use the default provider configuration. - * @return a {@code PrivateKey} - * @throws GeneralSecurityException if an error occurs parsing or - * decrypting the encrypted data, or producing the key object. - * @throws NullPointerException if {@code decryptKey} is null + * @param decryptKey the decryption key. Must not be {@code null}. + * @param provider the {@code Provider} for {@code Cipher} decryption + * and {@code PrivateKey} generation. If {@code null}, the + * default provider configuration is used. + * @return the decrypted {@code PrivateKey} + * @throws NullPointerException if {@code decryptKey} is {@code null} + * @throws NoSuchAlgorithmException if the decryption algorithm is unsupported + * @throws InvalidKeyException if an error occurs during parsing, + * decryption, or key generation * * @since 25 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public PrivateKey getKey(Key decryptKey, Provider provider) - throws GeneralSecurityException { - Objects.requireNonNull(decryptKey,"decryptKey cannot be null."); - PKCS8EncodedKeySpec p = getKeySpecImpl(decryptKey, provider); + throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(decryptKey,"a decryptKey must be specified."); + byte[] encoding = null; try { - if (provider == null) { - return KeyFactory.getInstance( - KeyUtil.getAlgorithm(p.getEncoded())). - generatePrivate(p); - } - return KeyFactory.getInstance(KeyUtil.getAlgorithm(p.getEncoded()), - provider).generatePrivate(p); - } catch (IOException e) { - throw new GeneralSecurityException(e); + encoding = decryptData(decryptKey, provider); + return PKCS8Key.parseKey(encoding, provider); + } finally { + KeyUtil.clear(encoding); } } + /** + * Extracts and returns the enclosed {@code KeyPair} using the specified + * password. If the encoded data does not contain both a public and private + * key, an {@code InvalidKeyException} is thrown. + * + * @param password the password used for PBE decryption. The array is cloned + * before use. + * @return a decrypted {@code KeyPair} + * @throws NullPointerException if {@code password} is {@code null} + * @throws NoSuchAlgorithmException if the decryption algorithm is unsupported + * @throws InvalidKeyException if the encoded data lacks a public key, or if + * an error occurs during parsing, decryption, or key generation + * + * @since 26 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public KeyPair getKeyPair(char[] password) + throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(password, "a password must be specified."); + + PBEKeySpec keySpec = new PBEKeySpec(password); + DEREncodable d; + try { + d = Pem.toDEREncodable(Pem.decryptEncoding(this, keySpec), true, null); + } finally { + keySpec.clearPassword(); + } + return switch (d) { + case KeyPair kp -> kp; + case PrivateKey ignored -> throw new InvalidKeyException( + "This encoding does not contain a public key."); + default -> throw new InvalidKeyException( + "Invalid class returned " + d.getClass().getName()); + }; + } + + /** + * Extracts and returns the enclosed {@code KeyPair} using the specified + * decryption key and provider. If the encoded data does not contain both a + * public and private key, an {@code InvalidKeyException} is thrown. + * + * @param decryptKey the decryption key. Must not be {@code null}. + * @param provider the {@code Provider} for {@code Cipher} decryption + * and key generation. If {@code null}, the default provider + * configuration is used. + * @return a decrypted {@code KeyPair} + * @throws NullPointerException if {@code decryptKey} is {@code null} + * @throws NoSuchAlgorithmException if the decryption algorithm is unsupported + * @throws InvalidKeyException if the encoded data lacks a public key, or if + * an error occurs during parsing, decryption, or key generation + * + * @since 26 + */ + @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) + public KeyPair getKeyPair(Key decryptKey, Provider provider) + throws NoSuchAlgorithmException, InvalidKeyException { + Objects.requireNonNull(decryptKey,"a decryptKey must be specified."); + + DEREncodable d = Pem.toDEREncodable( + decryptData(decryptKey, provider),true, provider); + return switch (d) { + case KeyPair kp -> kp; + case PrivateKey ignored -> throw new InvalidKeyException( + "This encoding does not contain a public key."); + default -> throw new InvalidKeyException( + "Invalid class returned " + d.getClass().getName()); + }; + } + /** * Extract the enclosed PKCS8EncodedKeySpec object from the * encrypted data and return it. @@ -585,7 +653,7 @@ public PKCS8EncodedKeySpec getKeySpec(Key decryptKey) * @param decryptKey key used for decrypting the encrypted data. * @param providerName the name of provider whose cipher * implementation will be used. - * @return the PKCS8EncodedKeySpec object. + * @return the PKCS8EncodedKeySpec object * @exception NullPointerException if {@code decryptKey} * or {@code providerName} is {@code null}. * @exception NoSuchProviderException if no provider @@ -670,17 +738,48 @@ public byte[] getEncoded() throws IOException { return this.encoded.clone(); } - private static void checkTag(DerValue val, byte tag, String valName) - throws IOException { - if (val.getTag() != tag) { - throw new IOException("invalid key encoding - wrong tag for " + - valName); - } - } - + // Read the encodedKey and return a P8EKS with the algorithm specified private static PKCS8EncodedKeySpec pkcs8EncodingToSpec(byte[] encodedKey) throws IOException { return new PKCS8EncodedKeySpec(encodedKey, KeyUtil.getAlgorithm(encodedKey)); } + + // Return the PKCS#8 encoding from a DEREncodable + private static byte[] getEncoding(DEREncodable d) { + return switch (d) { + case PrivateKey p -> p.getEncoded(); + case PKCS8EncodedKeySpec p8 -> p8.getEncoded(); + case KeyPair kp -> { + try { + yield PKCS8Key.getEncoded(kp.getPublic().getEncoded(), + kp.getPrivate().getEncoded()); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + default -> throw new IllegalArgumentException( + d.getClass().getName() + " not supported by this method"); + }; + } + + // Generate a SecretKey from the password. + private static SecretKey generateSecretKey(char[] password, String algorithm, + Provider provider) { + PBEKeySpec keySpec = new PBEKeySpec(password); + + try { + SecretKeyFactory factory; + if (provider == null) { + factory = SecretKeyFactory.getInstance(algorithm); + } else { + factory = SecretKeyFactory.getInstance(algorithm, provider); + } + return factory.generateSecret(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } finally { + keySpec.clearPassword(); + } + } } diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java index eb0346c7397ce..23a288bc1d6d1 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -87,7 +87,8 @@ public enum Feature { STRUCTURED_CONCURRENCY, @JEP(number = 502, title = "Stable Values", status = "Preview") STABLE_VALUES, - @JEP(number=470, title="PEM Encodings of Cryptographic Objects", status="Preview") + @JEP(number=524, title="PEM Encodings of Cryptographic Objects", + status="Second Preview") PEM_API, LANGUAGE_MODEL, /** diff --git a/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java b/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java index 2530425fbd410..a753a105e6e8c 100644 --- a/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/ECKeyFactory.java @@ -26,6 +26,7 @@ package sun.security.ec; import sun.security.pkcs.PKCS8Key; +import sun.security.util.KeyUtil; import java.security.*; import java.security.interfaces.*; @@ -213,11 +214,17 @@ private PublicKey implGeneratePublic(KeySpec keySpec) case ECPublicKeySpec e -> new ECPublicKeyImpl(e.getW(), e.getParams()); case PKCS8EncodedKeySpec p8 -> { - PKCS8Key p8key = new ECPrivateKeyImpl(p8.getEncoded()); - if (!p8key.hasPublicKey()) { - throw new InvalidKeySpecException("No public key found."); + byte[] encoded = p8.getEncoded(); + PKCS8Key p8key = null; + try { + p8key = new ECPrivateKeyImpl(encoded); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + yield new ECPublicKeyImpl(p8key.getPubKeyEncoded()); + } finally { + KeyUtil.clear(encoded, p8key); } - yield new ECPublicKeyImpl(p8key.getPubKeyEncoded()); } case null -> throw new InvalidKeySpecException( "keySpec must not be null"); diff --git a/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java b/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java index 65c4f093f27ea..1b69a5b22b496 100644 --- a/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java +++ b/src/java.base/share/classes/sun/security/ec/ECPrivateKeyImpl.java @@ -75,6 +75,7 @@ public final class ECPrivateKeyImpl extends PKCS8Key implements ECPrivateKey { @SuppressWarnings("serial") // Type of field is not Serializable private ECParameterSpec params; private byte[] domainParams; //Currently unsupported + private final byte SEC1v2 = 1; /** * Construct a key from its encoding. Called by the ECKeyFactory. @@ -92,34 +93,6 @@ public final class ECPrivateKeyImpl extends PKCS8Key implements ECPrivateKey { throws InvalidKeyException { this.s = s; this.params = params; - makeEncoding(s); - - } - - ECPrivateKeyImpl(byte[] s, ECParameterSpec params) - throws InvalidKeyException { - this.arrayS = s.clone(); - this.params = params; - makeEncoding(s); - } - - private void makeEncoding(byte[] s) throws InvalidKeyException { - algid = new AlgorithmId - (AlgorithmId.EC_oid, ECParameters.getAlgorithmParameters(params)); - DerOutputStream out = new DerOutputStream(); - out.putInteger(1); // version 1 - byte[] privBytes = s.clone(); - ArrayUtil.reverse(privBytes); - out.putOctetString(privBytes); - Arrays.fill(privBytes, (byte) 0); - DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); - privKeyMaterial = val.toByteArray(); - val.clear(); - } - - private void makeEncoding(BigInteger s) throws InvalidKeyException { - algid = new AlgorithmId(AlgorithmId.EC_oid, - ECParameters.getAlgorithmParameters(params)); byte[] sArr = s.toByteArray(); // convert to fixed-length array int numOctets = (params.getOrder().bitLength() + 7) / 8; @@ -129,11 +102,33 @@ private void makeEncoding(BigInteger s) throws InvalidKeyException { int length = Math.min(sArr.length, sOctets.length); System.arraycopy(sArr, inPos, sOctets, outPos, length); Arrays.fill(sArr, (byte) 0); + try { + makeEncoding(sOctets); + } finally { + Arrays.fill(sOctets, (byte) 0); + } + } + + ECPrivateKeyImpl(byte[] s, ECParameterSpec params) + throws InvalidKeyException { + this.arrayS = s.clone(); + this.params = params; + byte[] privBytes = arrayS.clone(); + ArrayUtil.reverse(privBytes); + try { + makeEncoding(privBytes); + } finally { + Arrays.fill(privBytes, (byte) 0); + } + + } + private void makeEncoding(byte[] privBytes) throws InvalidKeyException { + algid = new AlgorithmId(AlgorithmId.EC_oid, + ECParameters.getAlgorithmParameters(params)); DerOutputStream out = new DerOutputStream(); out.putInteger(1); // version 1 - out.putOctetString(sOctets); - Arrays.fill(sOctets, (byte) 0); + out.putOctetString(privBytes); DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); privKeyMaterial = val.toByteArray(); val.clear(); @@ -181,7 +176,7 @@ private void parseKeyBits() throws InvalidKeyException { } DerInputStream data = derValue.data; int version = data.getInteger(); - if (version != V2) { + if (version != SEC1v2) { throw new IOException("Version must be 1"); } byte[] privData = data.getOctetString(); @@ -253,4 +248,40 @@ private void readObject(ObjectInputStream stream) throw new InvalidObjectException( "ECPrivateKeyImpl keys are not directly deserializable"); } + + // Parse the SEC1v2 encoding to extract public key, if available. + public static BitArray parsePublicBits(byte[] privateBytes) { + DerValue seq = null; + try { + seq = new DerValue(privateBytes); + if (seq.tag == DerValue.tag_Sequence) { + int version = seq.data.getInteger(); + if (version == 1) { // EC + seq.data.getDerValue(); // read pass the private key + if (seq.data.available() != 0) { + DerValue derValue = seq.data.getDerValue(); + // check for optional [0] EC domain parameters + if (derValue.isContextSpecific((byte) 0)) { + if (seq.data.available() == 0) { + return null; + } + derValue = seq.data.getDerValue(); + } + // [1] public key + if (derValue.isContextSpecific((byte) 1)) { + derValue = derValue.data.getDerValue(); + return derValue.getUnalignedBitString(); + } + } + } + } + } catch (IOException e) { + throw new IllegalArgumentException(e); + } finally { + if (seq != null) { + seq.clear(); + } + } + return null; + } } diff --git a/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java b/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java index c8fb6c0c11bf7..8f060705323ef 100644 --- a/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/XDHKeyFactory.java @@ -26,6 +26,7 @@ package sun.security.ec; import sun.security.pkcs.PKCS8Key; +import sun.security.util.KeyUtil; import java.security.*; import java.security.interfaces.XECKey; @@ -159,15 +160,20 @@ private PublicKey generatePublicImpl(KeySpec keySpec) yield new XDHPublicKeyImpl(params, publicKeySpec.getU()); } case PKCS8EncodedKeySpec p8 -> { - PKCS8Key p8key = new XDHPrivateKeyImpl(p8.getEncoded()); - if (!p8key.hasPublicKey()) { - throw new InvalidKeySpecException("No public key found."); + byte[] encoded = p8.getEncoded(); + PKCS8Key p8key = new XDHPrivateKeyImpl(encoded); + try { + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + XDHPublicKeyImpl result = + new XDHPublicKeyImpl(p8key.getPubKeyEncoded()); + checkLockedParams(InvalidKeySpecException::new, + result.getParams()); + yield result; + } finally { + KeyUtil.clear(encoded, p8key); } - XDHPublicKeyImpl result = - new XDHPublicKeyImpl(p8key.getPubKeyEncoded()); - checkLockedParams(InvalidKeySpecException::new, - result.getParams()); - yield result; } case null -> throw new InvalidKeySpecException( "keySpec must not be null"); diff --git a/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java b/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java index 71ec14ba06f87..d59e44d81db25 100644 --- a/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java +++ b/src/java.base/share/classes/sun/security/ec/ed/EdDSAKeyFactory.java @@ -26,6 +26,7 @@ package sun.security.ec.ed; import sun.security.pkcs.PKCS8Key; +import sun.security.util.KeyUtil; import java.security.*; import java.security.interfaces.*; @@ -152,11 +153,17 @@ private PublicKey generatePublicImpl(KeySpec keySpec) yield new EdDSAPublicKeyImpl(params, publicKeySpec.getPoint()); } case PKCS8EncodedKeySpec p8 -> { - PKCS8Key p8key = new EdDSAPrivateKeyImpl(p8.getEncoded()); - if (!p8key.hasPublicKey()) { - throw new InvalidKeySpecException("No public key found."); + byte[] encoded = p8.getEncoded(); + PKCS8Key p8key = null; + try { + p8key = new EdDSAPrivateKeyImpl(encoded); + if (!p8key.hasPublicKey()) { + throw new InvalidKeySpecException("No public key found."); + } + yield new EdDSAPublicKeyImpl(p8key.getPubKeyEncoded()); + } finally { + KeyUtil.clear(encoded, p8key); } - yield new EdDSAPublicKeyImpl(p8key.getPubKeyEncoded()); } case null -> throw new InvalidKeySpecException( "keySpec must not be null"); diff --git a/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java b/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java index b7cc5e7057f53..dea87bd0a323d 100644 --- a/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java +++ b/src/java.base/share/classes/sun/security/pkcs/PKCS8Key.java @@ -26,6 +26,7 @@ package sun.security.pkcs; import jdk.internal.access.SharedSecrets; +import sun.security.ec.ECPrivateKeyImpl; import sun.security.util.*; import sun.security.x509.AlgorithmId; import sun.security.x509.X509Key; @@ -104,11 +105,29 @@ public PKCS8Key(byte[] input) throws InvalidKeyException { } } - private PKCS8Key(byte[] privEncoding, byte[] pubEncoding) + /** + * Constructor that takes both public and private encodings. + * + * If the private key includes a public key encoding (like an EC key in + * SEC1v2 format), and a specified public key matches it, the existing + * encoding is reused rather than recreated. + */ + public PKCS8Key(byte[] publicEncoding, byte[] privateEncoding) throws InvalidKeyException { - this(privEncoding); - pubKeyEncoded = pubEncoding; - version = V2; + this(privateEncoding); + if (publicEncoding != null) { + if (pubKeyEncoded != null) { + if (!Arrays.equals(pubKeyEncoded, publicEncoding)) { + Arrays.fill(privKeyMaterial, (byte) 0x0); + throw new InvalidKeyException("PrivateKey " + + "encoding has a public key that does not match " + + "the given PublicKey"); + } + } else { + pubKeyEncoded = publicEncoding; + version = V2; + } + } } public int getVersion() { @@ -137,6 +156,14 @@ private void decode(DerValue val) throws InvalidKeyException { // Store key material for subclasses to parse privKeyMaterial = val.data.getOctetString(); + // Special check and parsing for ECDSA's SEC1v2 format + if (algid.getOID().equals(AlgorithmId.EC_oid)) { + var bits = ECPrivateKeyImpl.parsePublicBits(privKeyMaterial); + if (bits != null) { + pubKeyEncoded = new X509Key(algid, bits).getEncoded(); + } + } + // PKCS8 v1 typically ends here if (val.data.available() == 0) { return; @@ -271,19 +298,24 @@ public String getFormat() { * With a given encoded Public and Private key, generate and return a * PKCS8v2 DER-encoded byte[]. * - * @param pubKeyEncoded DER-encoded PublicKey + * @param pubKeyEncoded DER-encoded PublicKey, this may be null. * @param privKeyEncoded DER-encoded PrivateKey * @return DER-encoded byte array * @throws IOException thrown on encoding failure */ public static byte[] getEncoded(byte[] pubKeyEncoded, byte[] privKeyEncoded) throws IOException { + PKCS8Key pkcs8Key; try { - return new PKCS8Key(privKeyEncoded, pubKeyEncoded). - generateEncoding(); + pkcs8Key = new PKCS8Key(pubKeyEncoded, privKeyEncoded); } catch (InvalidKeyException e) { throw new IOException(e); } + try { + return pkcs8Key.generateEncoding().clone(); + } finally { + pkcs8Key.clear(); + } } /** @@ -295,7 +327,7 @@ public static byte[] getEncoded(byte[] pubKeyEncoded, byte[] privKeyEncoded) private synchronized byte[] getEncodedInternal() { if (encodedKey == null) { try { - encodedKey = generateEncoding(); + generateEncoding(); } catch (IOException e) { return null; } @@ -326,7 +358,6 @@ private byte[] generateEncoding() throws IOException { throw new IOException(e); } - // X509Key x = X509Key.parse(pubKeyEncoded); DerOutputStream pubOut = new DerOutputStream(); pubOut.putUnalignedBitString(x.getKey()); out.writeImplicit( diff --git a/src/java.base/share/classes/sun/security/provider/X509Factory.java b/src/java.base/share/classes/sun/security/provider/X509Factory.java index 1a8ace55fc8f0..f732c7c045589 100644 --- a/src/java.base/share/classes/sun/security/provider/X509Factory.java +++ b/src/java.base/share/classes/sun/security/provider/X509Factory.java @@ -27,7 +27,7 @@ import java.io.*; -import java.security.PEMRecord; +import java.security.PEM; import java.security.cert.*; import java.util.*; @@ -559,7 +559,7 @@ private static byte[] readOneBlock(InputStream is) throws IOException { return bout.toByteArray(); } else { try { - PEMRecord rec; + PEM rec; try { rec = Pem.readPEM(is, (c == '-' ? true : false)); } catch (EOFException e) { diff --git a/src/java.base/share/classes/sun/security/util/KeyUtil.java b/src/java.base/share/classes/sun/security/util/KeyUtil.java index dd27b5f02d800..73e46d3e1f0f6 100644 --- a/src/java.base/share/classes/sun/security/util/KeyUtil.java +++ b/src/java.base/share/classes/sun/security/util/KeyUtil.java @@ -36,12 +36,14 @@ import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.DHPublicKeySpec; +import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import jdk.internal.access.SharedSecrets; import com.sun.crypto.provider.PBKDF2KeyImpl; import sun.security.jca.JCAUtil; +import sun.security.pkcs.PKCS8Key; import sun.security.x509.AlgorithmId; /** @@ -548,6 +550,22 @@ public static AlgorithmId getAlgorithmId(byte[] encoded) throws IOException { throw new IOException("No algorithm detected"); } - + // Generic method for zeroing arrays and objects + public static void clear(Object... list) { + for (Object o: list) { + switch (o) { + case byte[] b -> Arrays.fill(b, (byte)0); + case char[] c -> Arrays.fill(c, (char)0); + case PKCS8Key p8 -> p8.clear(); + case PKCS8EncodedKeySpec p8 -> + SharedSecrets.getJavaSecuritySpecAccess().clearEncodedKeySpec(p8); + case PBEKeySpec pbe -> pbe.clearPassword(); + case null -> {} + default -> + throw new IllegalArgumentException( + o.getClass().getName() + " not defined in KeyUtil.clear()"); + } + } + } } diff --git a/src/java.base/share/classes/sun/security/util/Pem.java b/src/java.base/share/classes/sun/security/util/Pem.java index 492017eca29fc..a9b2908bcc9b5 100644 --- a/src/java.base/share/classes/sun/security/util/Pem.java +++ b/src/java.base/share/classes/sun/security/util/Pem.java @@ -25,13 +25,18 @@ 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.NoSuchAlgorithmException; -import java.security.PEMRecord; -import java.security.Security; +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; @@ -145,6 +150,9 @@ private static String typeConverter(String 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 @@ -159,15 +167,16 @@ private static String typeConverter(String type) { * but the read position in the stream is at the end of the block, so * future reads can be successful. */ - public static PEMRecord readPEM(InputStream is, boolean shortHeader) + 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 starting hyphens + + // Find 5 hyphens followed by a 'B' to start processing the header. + boolean headerStarted = false; do { int d = is.read(); switch (d) { @@ -178,13 +187,20 @@ public static PEMRecord readPEM(InputStream is, boolean shortHeader) } throw new EOFException("No PEM data found"); } + case 'B' -> { + if (hyphen == 5) { + headerStarted = true; + } else { + hyphen = 0; + } + } default -> hyphen = 0; } os.write(d); - } while (hyphen != 5); + } while (!headerStarted); StringBuilder sb = new StringBuilder(64); - sb.append("-----"); + sb.append("-----B"); hyphen = 0; int c; @@ -307,14 +323,14 @@ public static PEMRecord readPEM(InputStream is, boolean shortHeader) // 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() > 5) { - preData = Arrays.copyOf(os.toByteArray(), os.size() - 5); + if (os.size() > 6) { + preData = Arrays.copyOf(os.toByteArray(), os.size() - 6); } - return new PEMRecord(typeConverter(headerType), data, preData); + return new PEM(typeConverter(headerType), data, preData); } - public static PEMRecord readPEM(InputStream is) throws IOException { + public static PEM readPEM(InputStream is) throws IOException { return readPEM(is, false); } @@ -342,8 +358,115 @@ public static String pemEncoded(String type, byte[] der) { * is not used with this method. * @return PEM in a string */ - public static String pemEncoded(PEMRecord pem) { + 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; + } + } diff --git a/test/jdk/java/security/PEM/PEMData.java b/test/jdk/java/security/PEM/PEMData.java index 2c8c60fccccf5..1c03baa7e7d0b 100644 --- a/test/jdk/java/security/PEM/PEMData.java +++ b/test/jdk/java/security/PEM/PEMData.java @@ -26,7 +26,7 @@ import javax.crypto.EncryptedPrivateKeyInfo; import java.security.DEREncodable; import java.security.KeyPair; -import java.security.PEMRecord; +import java.security.PEM; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.security.interfaces.*; @@ -48,6 +48,17 @@ class PEMData { -----END PRIVATE KEY----- """, KeyPair.class, "SunEC"); + // EC 256 with a domain parameter & public key + public static final Entry ecsecp256dom0 = new Entry("ecsecp256dom0", + """ + -----BEGIN PRIVATE KEY----- + MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgkW3Jx561NlEgBnut + KwDdi3cNwu7YYD/QtJ+9+AEBdoqgCgYIKoZIzj0DAQehRANCAASL+REY4vvAI9M3 + gonaml5K3lRgHq5w+OO4oO0VNduC44gUN1nrk7/wdNSpL+xXNEX52Dsff+2RD/fo + p224ANvB + -----END PRIVATE KEY----- + """, KeyPair.class, "SunEC"); + public static final Entry rsapriv = new Entry("rsapriv", """ -----BEGIN PRIVATE KEY----- @@ -149,7 +160,7 @@ class PEMData { -----END PRIVATE KEY----- """, RSAPrivateKey.class, "SunRsaSign"); - public static final Entry ec25519priv = new Entry("ed25519priv", + public static final Entry ed25519priv = new Entry("ed25519priv", """ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIFFZsmD+OKk67Cigc84/2fWtlKsvXWLSoMJ0MHh4jI4I @@ -189,6 +200,7 @@ class PEMData { -----END PUBLIC KEY----- """, RSAPublicKey.class, "SunRsaSign"); + // This is the public key contained in ecsecp256 public static final Entry ecsecp256pub = new Entry("ecsecp256pub", """ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u @@ -286,6 +298,19 @@ class PEMData { -----END RSA PRIVATE KEY----- """, RSAPrivateKey.class, "SunRsaSign"); + static final Entry ecsecp256ekpi = new Entry("ecsecp256ekpi", + """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBDhqUj1Oadj1GZXUMXT + b3QEAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBAgQQitxCfcZcMtoNu+X+ + PQk+/wSBkFL1NddKkUL2tRv6pNf1TR7eI7qJReGRgJexU/6pDN+UQS5e5qSySa7E + k1m2pUHgZlySUblXZj9nOzCsNFfq/jxlL15ZpAviAM2fRINnNEJcvoB+qZTS5cRb + Xs3wC7wymHW3EdIZ9sxfSHq9t7j9SnC1jGHjno0v1rKcdIvJtYloxsRYjsG/Sxhz + uNYnx8AMuQ== + -----END ENCRYPTED PRIVATE KEY----- + """, EncryptedPrivateKeyInfo.class, "SunEC", "fish".toCharArray()); + + static final Entry ed25519ep8 = new Entry("ed25519ep8", """ -----BEGIN ENCRYPTED PRIVATE KEY----- @@ -450,7 +475,7 @@ class PEMData { MQYMBGZpc2gwCgYIKoZIzj0EAwIDRwAwRAIgUBTdrMDE4BqruYRh1rRyKQBf48WR kIX8R4dBK9h1VRcCIEBR2Mzvku/huTbWTwKVlXBZeEmwIlxKwpRepPtViXcW -----END CERTIFICATE REQUEST----- - """, PEMRecord.class, "SunEC"); + """, PEM.class, "SunEC"); public static final String preData = "TEXT BLAH TEXT BLAH" + System.lineSeparator(); @@ -471,7 +496,7 @@ class PEMData { MQYMBGZpc2gwCgYIKoZIzj0EAwIDRwAwRAIgUBTdrMDE4BqruYRh1rRyKQBf48WR kIX8R4dBK9h1VRcCIEBR2Mzvku/huTbWTwKVlXBZeEmwIlxKwpRepPtViXcW -----END CERTIFICATE REQUEST----- - """ + postData, PEMRecord.class, "SunEC"); + """ + postData, PEM.class, "SunEC"); final static Pattern CR = Pattern.compile("\r"); final static Pattern LF = Pattern.compile("\n"); @@ -564,8 +589,9 @@ static public Entry getEntry(List list, String varname) { privList.add(rsapsspriv); privList.add(rsaprivbc); privList.add(ecsecp256); + privList.add(ecsecp256dom0); privList.add(ecsecp384); - privList.add(ec25519priv); + privList.add(ed25519priv); privList.add(ed25519ekpi); // The non-EKPI version needs decryption privList.add(rsaOpenSSL); oasList.add(oasrfc8410); diff --git a/test/jdk/java/security/PEM/PEMDecoderTest.java b/test/jdk/java/security/PEM/PEMDecoderTest.java index 8e3ae76994dc7..3ae088329b4b9 100644 --- a/test/jdk/java/security/PEM/PEMDecoderTest.java +++ b/test/jdk/java/security/PEM/PEMDecoderTest.java @@ -53,8 +53,11 @@ public class PEMDecoderTest { static HexFormat hex = HexFormat.of(); + static final PEMDecoder d = PEMDecoder.of(); public static void main(String[] args) throws Exception { + PEMDecoder decr; + System.out.println("Decoder test:"); PEMData.entryList.forEach(entry -> test(entry, false)); System.out.println("Decoder test withFactory:"); @@ -89,12 +92,12 @@ public static void main(String[] args) throws Exception { System.out.println("Decoder test ecsecp256:"); testFailure(PEMData.ecsecp256pub.makeNoCRLF("pubecpem-no")); System.out.println("Decoder test RSAcert with decryption Decoder:"); - PEMDecoder d = PEMDecoder.of().withDecryption("123".toCharArray()); - d.decode(PEMData.rsaCert.pem()); + decr = d.withDecryption("123".toCharArray()); + decr.decode(PEMData.rsaCert.pem()); System.out.println("Decoder test ecsecp256 private key with decryption Decoder:"); - ((KeyPair) d.decode(PEMData.ecsecp256.pem())).getPrivate(); + ((KeyPair) decr.decode(PEMData.ecsecp256.pem())).getPrivate(); System.out.println("Decoder test ecsecp256 to P8EKS:"); - d.decode(PEMData.ecsecp256.pem(), PKCS8EncodedKeySpec.class); + decr.decode(PEMData.ecsecp256.pem(), PKCS8EncodedKeySpec.class); System.out.println("Checking if decode() returns the same encoding:"); PEMData.privList.forEach(PEMDecoderTest::testDERCheck); @@ -111,11 +114,11 @@ public static void main(String[] args) throws Exception { System.out.println("Checking if ecCSR:"); test(PEMData.ecCSR); System.out.println("Checking if ecCSR with preData:"); - DEREncodable result = PEMDecoder.of().decode(PEMData.ecCSRWithData.pem(), PEMRecord.class); - if (result instanceof PEMRecord rec) { + DEREncodable result = d.decode(PEMData.ecCSRWithData.pem(), PEM.class); + if (result instanceof PEM rec) { if (PEMData.preData.compareTo(new String(rec.leadingData())) != 0) { - System.err.println("expected: " + PEMData.preData); - System.err.println("received: " + new String(rec.leadingData())); + System.err.println("expected: \"" + PEMData.preData + "\""); + System.err.println("received: \"" + new String(rec.leadingData()) + "\""); throw new AssertionError("ecCSRWithData preData wrong"); } if (rec.content().lastIndexOf("F") > rec.content().length() - 5) { @@ -128,35 +131,34 @@ public static void main(String[] args) throws Exception { } System.out.println("Decoding RSA pub using class PEMRecord:"); - result = PEMDecoder.of().decode(PEMData.rsapub.pem(), PEMRecord.class); - if (!(result instanceof PEMRecord)) { + result = d.decode(PEMData.rsapub.pem(), PEM.class); + if (!(result instanceof PEM)) { throw new AssertionError("pubecpem didn't return a PEMRecord"); } - if (((PEMRecord) result).type().compareTo(Pem.PUBLIC_KEY) != 0) { + if (((PEM) result).type().compareTo(Pem.PUBLIC_KEY) != 0) { throw new AssertionError("pubecpem PEMRecord didn't decode as a Public Key"); } testInputStream(); testPEMRecord(PEMData.rsapub); testPEMRecord(PEMData.ecCert); - testPEMRecord(PEMData.ec25519priv); + testPEMRecord(PEMData.ed25519priv); testPEMRecord(PEMData.ecCSR); testPEMRecord(PEMData.ecCSRWithData); testPEMRecordDecode(PEMData.rsapub); testPEMRecordDecode(PEMData.ecCert); - testPEMRecordDecode(PEMData.ec25519priv); + testPEMRecordDecode(PEMData.ed25519priv); testPEMRecordDecode(PEMData.ecCSR); testPEMRecordDecode(PEMData.ecCSRWithData); - d = PEMDecoder.of(); System.out.println("Check leadingData is null with back-to-back PEMs: "); - String s = new PEMRecord("ONE", "1212").toString() - + new PEMRecord("TWO", "3434").toString(); - var ins = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); - if (d.decode(ins, PEMRecord.class).leadingData() != null) { + String s = new PEM("ONE", "1212").toString() + + new PEM("TWO", "3434").toString(); + ByteArrayInputStream bis = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + if (d.decode(bis, PEM.class).leadingData() != null) { throw new AssertionError("leading data not null on first pem"); } - if (d.decode(ins, PEMRecord.class).leadingData() != null) { + if (d.decode(bis, PEM.class).leadingData() != null) { throw new AssertionError("leading data not null on second pem"); } System.out.println("PASS"); @@ -173,8 +175,8 @@ public static void main(String[] args) throws Exception { } // PBE - System.out.println("EncryptedPrivateKeyInfo.encryptKey with PBE: "); - ekpi = EncryptedPrivateKeyInfo.encryptKey(privateKey, + System.out.println("EncryptedPrivateKeyInfo.encrypt with PBE: "); + ekpi = EncryptedPrivateKeyInfo.encrypt(privateKey, "password".toCharArray(),"PBEWithMD5AndDES", null, null); try { ekpi.getKey("password".toCharArray()); @@ -184,8 +186,8 @@ public static void main(String[] args) throws Exception { } // PBES2 - System.out.println("EncryptedPrivateKeyInfo.encryptKey with default: "); - ekpi = EncryptedPrivateKeyInfo.encryptKey(privateKey + System.out.println("EncryptedPrivateKeyInfo.encrypt with default: "); + ekpi = EncryptedPrivateKeyInfo.encrypt(privateKey , "password".toCharArray()); try { ekpi.getKey("password".toCharArray()); @@ -197,6 +199,13 @@ public static void main(String[] args) throws Exception { System.out.println("Decoder test testCoefZero:"); testCoefZero(PEMData.rsaCrtCoefZeroPriv); + + // leadingData can contain dashes + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write("--------\n".getBytes(StandardCharsets.ISO_8859_1)); + bos.write(PEMData.ecsecp256ekpi.pem().getBytes(StandardCharsets.ISO_8859_1)); + bis = new ByteArrayInputStream(bos.toByteArray()); + result = d.decode(bis, PEM.class); } static void testInputStream() throws IOException { @@ -211,10 +220,10 @@ static void testInputStream() throws IOException { ByteArrayInputStream is = new ByteArrayInputStream(ba.toByteArray()); System.out.println("Decoding 2 RSA pub with pre & post data:"); - PEMRecord obj; + PEM obj; int keys = 0; while (keys++ < 2) { - obj = PEMDecoder.of().decode(is, PEMRecord.class); + obj = d.decode(is, PEM.class); if (!PEMData.preData.equalsIgnoreCase( new String(obj.leadingData()))) { System.out.println("expected: \"" + PEMData.preData + "\""); @@ -225,7 +234,7 @@ static void testInputStream() throws IOException { System.out.println(" Read public key."); } try { - PEMDecoder.of().decode(is, PEMRecord.class); + d.decode(is, PEM.class); throw new AssertionError("3rd entry returned a PEMRecord"); } catch (EOFException e) { System.out.println("Success: No 3rd entry found. EOFE thrown."); @@ -234,7 +243,7 @@ static void testInputStream() throws IOException { // End of stream try { System.out.println("Failed: There should be no PEMRecord: " + - PEMDecoder.of().decode(is, PEMRecord.class)); + d.decode(is, PEM.class)); } catch (EOFException e) { System.out.println("Success"); return; @@ -250,22 +259,22 @@ static void testInputStream() throws IOException { static void testCertTypeConverter(PEMData.Entry entry) throws CertificateEncodingException { String certPem = entry.pem().replace("CERTIFICATE", "X509 CERTIFICATE"); Asserts.assertEqualsByteArray(entry.der(), - PEMDecoder.of().decode(certPem, X509Certificate.class).getEncoded()); + d.decode(certPem, X509Certificate.class).getEncoded()); certPem = entry.pem().replace("CERTIFICATE", "X.509 CERTIFICATE"); Asserts.assertEqualsByteArray(entry.der(), - PEMDecoder.of().decode(certPem, X509Certificate.class).getEncoded()); + d.decode(certPem, X509Certificate.class).getEncoded()); } // test that when the crtCoeff is zero, the key is decoded but only the modulus and private // exponent are used resulting in a different der static void testCoefZero(PEMData.Entry entry) { - RSAPrivateKey decoded = PEMDecoder.of().decode(entry.pem(), RSAPrivateKey.class); + RSAPrivateKey decoded = d.decode(entry.pem(), RSAPrivateKey.class); Asserts.assertNotEqualsByteArray(decoded.getEncoded(), entry.der()); } static void testPEMRecord(PEMData.Entry entry) { - PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class); + PEM r = d.decode(entry.pem(), PEM.class); String expected = entry.pem().split("-----")[2].replace(System.lineSeparator(), ""); try { PEMData.checkResults(expected, r.content()); @@ -285,7 +294,7 @@ static void testPEMRecord(PEMData.Entry entry) { case Pem.X509_CRL -> entry.clazz().isAssignableFrom(X509CRL.class); case "CERTIFICATE REQUEST" -> - entry.clazz().isAssignableFrom(PEMRecord.class); + entry.clazz().isAssignableFrom(PEM.class); default -> false; }; @@ -300,8 +309,8 @@ static void testPEMRecord(PEMData.Entry entry) { static void testPEMRecordDecode(PEMData.Entry entry) { - PEMRecord r = PEMDecoder.of().decode(entry.pem(), PEMRecord.class); - DEREncodable de = PEMDecoder.of().decode(r.toString()); + PEM r = d.decode(entry.pem(), PEM.class); + DEREncodable de = d.decode(r.toString()); boolean result = switch(r.type()) { case Pem.PRIVATE_KEY -> @@ -311,7 +320,7 @@ static void testPEMRecordDecode(PEMData.Entry entry) { case Pem.CERTIFICATE, Pem.X509_CERTIFICATE -> (de instanceof X509Certificate); case Pem.X509_CRL -> (de instanceof X509CRL); - case "CERTIFICATE REQUEST" -> (de instanceof PEMRecord); + case "CERTIFICATE REQUEST" -> (de instanceof PEM); default -> false; }; @@ -332,7 +341,7 @@ static void testFailure(PEMData.Entry entry) { static void testFailure(PEMData.Entry entry, Class c) { try { - test(entry.pem(), c, PEMDecoder.of()); + test(entry.pem(), c, d); if (entry.pem().indexOf('\r') != -1) { System.out.println("Found a CR."); } @@ -351,9 +360,11 @@ static void testFailure(PEMData.Entry entry, Class c) { } static DEREncodable testEncrypted(PEMData.Entry entry) { - PEMDecoder decoder = PEMDecoder.of(); + PEMDecoder decoder; if (!Objects.equals(entry.clazz(), EncryptedPrivateKeyInfo.class)) { - decoder = decoder.withDecryption(entry.password()); + decoder = d.withDecryption(entry.password()); + } else { + decoder = d; } try { @@ -381,9 +392,9 @@ static DEREncodable test(PEMData.Entry entry, boolean withFactory) { PEMDecoder pemDecoder; if (withFactory) { Provider provider = Security.getProvider(entry.provider()); - pemDecoder = PEMDecoder.of().withFactory(provider); + pemDecoder = d.withFactory(provider); } else { - pemDecoder = PEMDecoder.of(); + pemDecoder = d; } DEREncodable r = test(entry.pem(), entry.clazz(), pemDecoder); System.out.println("PASS (" + entry.name() + ")"); @@ -446,9 +457,8 @@ static DEREncodable test(String pem, Class clazz, PEMDecoder decoder) // result is the same static void testTwoKeys() throws IOException { PublicKey p1, p2; - PEMDecoder pd = PEMDecoder.of(); - p1 = pd.decode(PEMData.rsapub.pem(), RSAPublicKey.class); - p2 = pd.decode(PEMData.rsapub.pem(), RSAPublicKey.class); + p1 = d.decode(PEMData.rsapub.pem(), RSAPublicKey.class); + p2 = d.decode(PEMData.rsapub.pem(), RSAPublicKey.class); if (!Arrays.equals(p1.getEncoded(), p2.getEncoded())) { System.err.println("These two should have matched:"); System.err.println(hex.parseHex(new String(p1.getEncoded()))); @@ -460,7 +470,7 @@ static void testTwoKeys() throws IOException { private static void testPKCS8Key(PEMData.Entry entry) { try { - PKCS8Key key = PEMDecoder.of().decode(entry.pem(), PKCS8Key.class); + PKCS8Key key = d.decode(entry.pem(), PKCS8Key.class); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(key.getEncoded()); @@ -472,7 +482,7 @@ private static void testPKCS8Key(PEMData.Entry entry) { } static void testClass(PEMData.Entry entry, Class clazz) throws IOException { - var pk = PEMDecoder.of().decode(entry.pem(), clazz); + var pk = d.decode(entry.pem(), clazz); } static void testClass(PEMData.Entry entry, Class clazz, boolean pass) @@ -506,7 +516,7 @@ static void testDERCheck(PEMData.Entry entry) { return; } - PKCS8EncodedKeySpec p8 = PEMDecoder.of().decode(entry.pem(), + PKCS8EncodedKeySpec p8 = d.decode(entry.pem(), PKCS8EncodedKeySpec.class); int result = Arrays.compare(entry.der(), p8.getEncoded()); if (result != 0) { @@ -531,8 +541,8 @@ static void testSignature(PEMData.Entry entry) { byte[] data = "12345678".getBytes(); PrivateKey privateKey; - DEREncodable d = PEMDecoder.of().decode(entry.pem()); - switch (d) { + DEREncodable der = d.decode(entry.pem()); + switch (der) { case PrivateKey p -> privateKey = p; case KeyPair kp -> privateKey = kp.getPrivate(); case EncryptedPrivateKeyInfo e -> { @@ -563,7 +573,7 @@ static void testSignature(PEMData.Entry entry) { }; try { - if (d instanceof PrivateKey) { + if (der instanceof PrivateKey) { s = Signature.getInstance(algorithm); if (spec != null) { s.setParameter(spec); @@ -572,12 +582,12 @@ static void testSignature(PEMData.Entry entry) { s.update(data); s.sign(); System.out.println("PASS (Sign): " + entry.name()); - } else if (d instanceof KeyPair) { + } else if (der instanceof KeyPair) { s = Signature.getInstance(algorithm); s.initSign(privateKey); s.update(data); byte[] sig = s.sign(); - s.initVerify(((KeyPair)d).getPublic()); + s.initVerify(((KeyPair)der).getPublic()); s.verify(sig); System.out.println("PASS (Sign/Verify): " + entry.name()); } else { diff --git a/test/jdk/java/security/PEM/PEMEncoderTest.java b/test/jdk/java/security/PEM/PEMEncoderTest.java index 3d1948ba2fed6..4b60758c89fc7 100644 --- a/test/jdk/java/security/PEM/PEMEncoderTest.java +++ b/test/jdk/java/security/PEM/PEMEncoderTest.java @@ -62,6 +62,10 @@ public class PEMEncoderTest { public static void main(String[] args) throws Exception { pkcs8DefaultAlgExpect = args[0]; PEMEncoder encoder = PEMEncoder.of(); + PEMDecoder decoder = PEMDecoder.of(); + EncryptedPrivateKeyInfo ekpi; + KeyPair kp; + PEM pem; // These entries are removed var newEntryList = new ArrayList<>(PEMData.entryList); @@ -70,14 +74,13 @@ public static void main(String[] args) throws Exception { newEntryList.remove(PEMData.getEntry("ecsecp384")); keymap = generateObjKeyMap(newEntryList); System.out.println("Same instance re-encode test:"); - keymap.keySet().stream().forEach(key -> test(key, encoder)); + keymap.keySet().forEach(key -> test(key, encoder)); System.out.println("New instance re-encode test:"); - keymap.keySet().stream().forEach(key -> test(key, PEMEncoder.of())); + keymap.keySet().forEach(key -> test(key, PEMEncoder.of())); System.out.println("Same instance re-encode testToString:"); - keymap.keySet().stream().forEach(key -> testToString(key, encoder)); + keymap.keySet().forEach(key -> testToString(key, encoder)); System.out.println("New instance re-encode testToString:"); - keymap.keySet().stream().forEach(key -> testToString(key, - PEMEncoder.of())); + keymap.keySet().forEach(key -> testToString(key, PEMEncoder.of())); System.out.println("Same instance Encoder testEncodedKeySpec:"); testEncodedKeySpec(encoder); System.out.println("New instance Encoder testEncodedKeySpec:"); @@ -86,14 +89,14 @@ public static void main(String[] args) throws Exception { testEmptyAndNullKey(encoder); keymap = generateObjKeyMap(PEMData.encryptedList); System.out.println("Same instance Encoder match test:"); - keymap.keySet().stream().forEach(key -> testEncryptedMatch(key, encoder)); + keymap.keySet().forEach(key -> testEncryptedMatch(key, encoder)); System.out.println("Same instance Encoder new withEnc test:"); - keymap.keySet().stream().forEach(key -> testEncrypted(key, encoder)); + keymap.keySet().forEach(key -> testEncrypted(key, encoder)); System.out.println("New instance Encoder and withEnc test:"); - keymap.keySet().stream().forEach(key -> testEncrypted(key, PEMEncoder.of())); + keymap.keySet().forEach(key -> testEncrypted(key, PEMEncoder.of())); System.out.println("Same instance encrypted Encoder test:"); PEMEncoder encEncoder = encoder.withEncryption("fish".toCharArray()); - keymap.keySet().stream().forEach(key -> testSameEncryptor(key, encEncoder)); + keymap.keySet().forEach(key -> testSameEncryptor(key, encEncoder)); try { encoder.withEncryption(null); } catch (Exception e) { @@ -102,17 +105,51 @@ public static void main(String[] args) throws Exception { } } - PEMDecoder d = PEMDecoder.of(); - PEMRecord pemRecord = - d.decode(PEMData.ed25519ep8.pem(), PEMRecord.class); - PEMData.checkResults(PEMData.ed25519ep8, pemRecord.toString()); + pem = decoder.decode(PEMData.ed25519ep8.pem(), PEM.class); + PEMData.checkResults(PEMData.ed25519ep8, pem.toString()); - // test PemRecord is encapsulated with PEM header and footer on encoding + // test PEM is encapsulated with PEM header and footer on encoding String[] pemLines = PEMData.ed25519ep8.pem().split("\n"); String[] pemNoHeaderFooter = Arrays.copyOfRange(pemLines, 1, pemLines.length - 1); - PEMRecord pemR = new PEMRecord("ENCRYPTED PRIVATE KEY", String.join("\n", + pem = new PEM("ENCRYPTED PRIVATE KEY", String.join("\n", pemNoHeaderFooter)); - PEMData.checkResults(PEMData.ed25519ep8.pem(), encoder.encodeToString(pemR)); + PEMData.checkResults(PEMData.ed25519ep8.pem(), encoder.encodeToString(pem)); + + // Verify the same private key bytes are returned with an ECDSA private + // key PEM and an encrypted PEM. + kp = decoder.decode(PEMData.ecsecp256.pem(), KeyPair.class); + var origPriv = kp.getPrivate(); + String s = encoder.withEncryption(PEMData.ecsecp256ekpi.password()).encodeToString(kp); + kp = decoder.withDecryption(PEMData.ecsecp256ekpi.password()).decode(s, KeyPair.class); + var newPriv = kp.getPrivate(); + if (!Arrays.equals(origPriv.getEncoded(), newPriv.getEncoded())) { + throw new AssertionError("compare fails"); + } + + // Encoded non-encrypted Keypair + kp = KeyPairGenerator.getInstance("XDH").generateKeyPair(); + s = encoder.encodeToString(kp); + decoder.decode(s, KeyPair.class); + + // EmptyKey for the PrivateKey in a KeyPair. Uses keypair from above. + try { + encoder.encode(new KeyPair(kp.getPublic(), new EmptyKey())); + throw new AssertionError("encoder accepted a empty private key encoding"); + } catch (IllegalArgumentException _) {} + + // NullKey for the PrivateKey in a KeyPair. Uses keypair from above. + try { + encoder.encode(new KeyPair(kp.getPublic(), new NullKey())); + throw new AssertionError("encoder accepted a empty private key encoding"); + } catch (IllegalArgumentException _) {} + + ekpi = decoder.decode(PEMData.ecsecp256ekpi.pem(), + EncryptedPrivateKeyInfo.class); + try { + encoder.withEncryption("blah".toCharArray()).encode(ekpi); + throw new AssertionError("encoder tried to encrypt " + + "an EncryptedPrivateKeyInfo."); + } catch (IllegalArgumentException _) {} } static Map generateObjKeyMap(List list) { @@ -215,7 +252,7 @@ static void testEncryptedMatch(String key, PEMEncoder encoder) { EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(entry.pem(), EncryptedPrivateKeyInfo.class); if (entry.password() != null) { - EncryptedPrivateKeyInfo.encryptKey(pkey, entry.password(), + EncryptedPrivateKeyInfo.encrypt(pkey, entry.password(), Pem.DEFAULT_ALGO, ekpi.getAlgParameters(). getParameterSpec(PBEParameterSpec.class), null); @@ -267,4 +304,16 @@ private static class EmptyKey implements PublicKey, PrivateKey { @Override public byte[] getEncoded() { return new byte[0]; } } + + private static class NullKey implements PrivateKey { + @Override + public String getAlgorithm() { return "Test"; } + + @Override + public String getFormat() { return "Test"; } + + @Override + public byte[] getEncoded() { return null; } + } + } diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java similarity index 76% rename from test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java rename to test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java index 3fe8cfcfbfa87..abeed7c3395d1 100644 --- a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/EncryptKey.java +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java @@ -48,7 +48,7 @@ import static jdk.test.lib.Asserts.assertEquals; -public class EncryptKey { +public class Encrypt { private static final String encEdECKey = """ @@ -74,7 +74,7 @@ public static void main(String[] args) throws Exception { AlgorithmParameters ap = ekpi.getAlgParameters(); // Test encryptKey(PrivateKey, char[], String, ... ) - var e = EncryptedPrivateKeyInfo.encryptKey(priKey, password, + var e = EncryptedPrivateKeyInfo.encrypt(priKey, password, ekpi.getAlgName(), ap.getParameterSpec(PBEParameterSpec.class), null); if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { @@ -83,45 +83,53 @@ public static void main(String[] args) throws Exception { } // Test encryptKey(PrivateKey, char[], String, ...) with provider - e = EncryptedPrivateKeyInfo.encryptKey(priKey, password, ekpi.getAlgName(), - ap.getParameterSpec(PBEParameterSpec.class), p); + e = EncryptedPrivateKeyInfo.encrypt(priKey, password, ekpi.getAlgName(), + ap.getParameterSpec(PBEParameterSpec.class), p); if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { throw new AssertionError("encryptKey() didn't match" + - " with expected."); + " with expected."); } // Test encryptKey(PrivateKey, char[], String, ...) with provider and null algorithm - e = EncryptedPrivateKeyInfo.encryptKey(priKey, password, null, null, - p); + e = EncryptedPrivateKeyInfo.encrypt(priKey, password, Pem.DEFAULT_ALGO, null, p); assertEquals(e.getAlgName(), Pem.DEFAULT_ALGO); // Test encryptKey(PrivateKey, Key, String, ...) - e = EncryptedPrivateKeyInfo.encryptKey(priKey, key, ekpi.getAlgName(), - ap.getParameterSpec(PBEParameterSpec.class),null, null); + e = EncryptedPrivateKeyInfo.encrypt(priKey, key, ekpi.getAlgName(), + ap.getParameterSpec(PBEParameterSpec.class), null, null); if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { throw new AssertionError("encryptKey() didn't match" + " with expected."); } // Test encryptKey(PrivateKey, Key, String, ...) with provider and null random - e = EncryptedPrivateKeyInfo.encryptKey(priKey, key, ekpi.getAlgName(), - ap.getParameterSpec(PBEParameterSpec.class), p, null); + e = EncryptedPrivateKeyInfo.encrypt(priKey, key, ekpi.getAlgName(), + ap.getParameterSpec(PBEParameterSpec.class), p, null); if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { throw new AssertionError("encryptKey() didn't match" + - " with expected."); + " with expected."); } // Test encryptKey(PrivateKey, Key, String, ...) with provider and SecureRandom - e = EncryptedPrivateKeyInfo.encryptKey(priKey, key, ekpi.getAlgName(), - ap.getParameterSpec(PBEParameterSpec.class), p, new SecureRandom()); + e = EncryptedPrivateKeyInfo.encrypt(priKey, key, ekpi.getAlgName(), + ap.getParameterSpec(PBEParameterSpec.class), p, new SecureRandom()); if (!Arrays.equals(ekpi.getEncryptedData(), e.getEncryptedData())) { throw new AssertionError("encryptKey() didn't match" + - " with expected."); + " with expected."); } // Test encryptKey(PrivateKey, Key, String, ...) with provider and null algorithm - e = EncryptedPrivateKeyInfo.encryptKey(priKey, key, null, null, - p, new SecureRandom()); + e = EncryptedPrivateKeyInfo.encrypt(priKey, key, Pem.DEFAULT_ALGO, null, + p, new SecureRandom()); assertEquals(e.getAlgName(), Pem.DEFAULT_ALGO); + + + SecretKey key2 = new SecretKeySpec("1234567890123456".getBytes(), "AES"); + + // Test encryptKey(PrivateKey, Key, String, ...) with provider and SecureRandom + e = EncryptedPrivateKeyInfo.encrypt(priKey, key2, "AES_128/GCM/NoPadding", + null, p, new SecureRandom()); + PrivateKey key3 = e.getKey(key2, null); + assertEquals(key3, priKey, "AES encryption failed"); } } diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java index b1917ffa84d7f..36e6b02faba0d 100644 --- a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java @@ -71,7 +71,8 @@ public class GetKey { passwdText.getBytes(), "PBE"); public static void main(String[] args) throws Exception { - Provider p = Security.getProvider(System.getProperty("test.provider.name", "SunJCE")); + Provider p = Security.getProvider( + System.getProperty("test.provider.name", "SunJCE")); EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(encEdECKey, EncryptedPrivateKeyInfo.class); diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java new file mode 100644 index 0000000000000..d35197e1971cb --- /dev/null +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 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. + */ + +/* + * @test + * @bug 8360563 + * @library /test/lib + * @summary Testing getKeyPair using ML-KEM + * @enablePreview + * @modules java.base/sun.security.util + */ + +import jdk.test.lib.Asserts; +import sun.security.util.DerValue; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.*; + +/* + * This generates an ML-KEM key pair and makes it into PEM data. By using + * PEM, it constructs a OneAsymmetricKey structure that combines + * the public key into the private key encoding. Decode the PEM data into + * a KeyPair and an EKPI for verification. + * + * The original private key does not have the public key encapsulated, so it + * cannot be used for verification. + * + * Verify the decoded PEM KeyPair and EKPI.getKeyPair() return matching public + * and private keys encodings; as well as, verify the original public key + * matches. + */ + +public class GetKeyPair { + private static final String passwdText = "fish"; + private static final char[] password = passwdText.toCharArray(); + private static final SecretKey key = new SecretKeySpec( + passwdText.getBytes(), "PBE"); + static byte[] keyOrigPub, keyOrigPriv; + + public static void main(String[] args) throws Exception { + Provider p = Security.getProvider( + System.getProperty("test.provider.name", "SunJCE")); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM"); + KeyPair kpOrig = kpg.generateKeyPair(); + keyOrigPub = kpOrig.getPublic().getEncoded(); + keyOrigPriv = getPrivateKey(kpOrig.getPrivate()); + + // Encode the KeyPair into PEM, constructing an OneAsymmetricKey encoding + String pem = PEMEncoder.of().withEncryption(password). + encodeToString(kpOrig); + // Decode to a KeyPair from the generated PEM for verification. + KeyPair mlkemKP = PEMDecoder.of().withDecryption(password). + decode(pem, KeyPair.class); + + // Check decoded public key pair with original. + Asserts.assertEqualsByteArray(mlkemKP.getPublic().getEncoded(), + keyOrigPub, "Initial PublicKey compare didn't match."); + byte[] priv = getPrivateKey(mlkemKP.getPrivate()); + Asserts.assertEqualsByteArray(priv, keyOrigPriv, + "Initial PrivateKey compare didn't match"); + + // Decode to a EncryptedPrivateKeyInfo. + EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(pem, + EncryptedPrivateKeyInfo.class); + + // Test getKeyPair(password) + System.out.print("Testing getKeyPair(char[]): "); + arrayCheck(ekpi.getKeyPair(password)); + + // Test getKeyPair(key, provider) provider null + System.out.print("Testing getKeyPair(key, null): "); + arrayCheck(ekpi.getKeyPair(key, null)); + + // Test getKeyPair(key, provider) provider SunJCE + System.out.print("Testing getKeyPair(key, SunJCE): "); + arrayCheck(ekpi.getKeyPair(key, p)); + } + + static void arrayCheck(KeyPair kp) throws Exception { + Asserts.assertEqualsByteArray(getPrivateKey(kp.getPrivate()), keyOrigPriv, + "PrivateKey didn't match with expected."); + Asserts.assertEqualsByteArray(kp.getPublic().getEncoded(), keyOrigPub, + "PublicKey didn't match with expected."); + System.out.println("PASS"); + } + + static byte[] getPrivateKey(PrivateKey p) throws Exception{ + var val = new DerValue(p.getEncoded()); + // Get version + val.data.getInteger(); + // Get AlgorithmID + val.data.getDerValue(); + // Return PrivateKey + return val.data.getOctetString(); + } +} diff --git a/test/jdk/javax/net/ssl/interop/ClientHelloInterOp.java b/test/jdk/javax/net/ssl/interop/ClientHelloInterOp.java index 808d137223e96..80e50d0116043 100644 --- a/test/jdk/javax/net/ssl/interop/ClientHelloInterOp.java +++ b/test/jdk/javax/net/ssl/interop/ClientHelloInterOp.java @@ -31,7 +31,6 @@ import java.nio.ByteBuffer; import java.security.KeyStore; import java.security.PEMDecoder; -import java.security.PEMRecord; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate;