From e70e6913077190b4ffaff886ca9a5f91eed3f850 Mon Sep 17 00:00:00 2001 From: Anthony Scarpino Date: Wed, 3 Jun 2026 17:06:31 +0000 Subject: [PATCH] 8377506: Implement JEP 538: PEM Encodings of Cryptographic Objects (Third Preview) Reviewed-by: weijun, mullan --- .../classes/java/security/AsymmetricKey.java | 4 +- ...DEREncodable.java => BinaryEncodable.java} | 20 +- .../share/classes/java/security/KeyPair.java | 4 +- .../share/classes/java/security/PEM.java | 228 ++++++--- .../classes/java/security/PEMDecoder.java | 390 ++++++++------- .../classes/java/security/PEMEncoder.java | 225 ++++----- .../classes/java/security/cert/X509CRL.java | 4 +- .../java/security/cert/X509Certificate.java | 4 +- .../security/spec/PKCS8EncodedKeySpec.java | 6 +- .../security/spec/X509EncodedKeySpec.java | 6 +- .../classes/javax/crypto/CryptoException.java | 99 ++++ .../javax/crypto/EncryptedPrivateKeyInfo.java | 227 +++++---- .../jdk/internal/javac/PreviewFeature.java | 4 +- .../sun/security/provider/X509Factory.java | 2 +- .../classes/sun/security/util/KeyUtil.java | 21 + .../share/classes/sun/security/util/Pem.java | 460 ++++++++++-------- test/jdk/java/security/PEM/PEMData.java | 96 +++- .../jdk/java/security/PEM/PEMDecoderTest.java | 59 ++- .../jdk/java/security/PEM/PEMEncoderTest.java | 83 +++- .../selfIssued/DisableRevocation.java | 8 +- .../selfIssued/KeyUsageMatters.java | 10 +- .../selfIssued/StatusLoopDependency.java | 10 +- .../CertPathValidator/OCSP/FailoverToCRL.java | 8 +- .../indirectCRL/CircularCRLOneLevel.java | 8 +- .../CircularCRLOneLevelRevoked.java | 8 +- .../indirectCRL/CircularCRLTwoLevel.java | 8 +- .../CircularCRLTwoLevelRevoked.java | 8 +- .../EncryptedPrivateKeyInfo/Encrypt.java | 4 +- .../EncryptedPrivateKeyInfo/GetKey.java | 41 +- .../EncryptedPrivateKeyInfo/GetKeyPair.java | 13 +- .../security/provider/X509Factory/BadPem.java | 7 +- .../DisabledAlgorithms/CPBuilder.java | 8 +- .../DisabledAlgorithms/CPBuilderWithMD5.java | 8 +- .../security/rsa/pss/PSSKeyCompatibility.java | 4 +- 34 files changed, 1259 insertions(+), 836 deletions(-) rename src/java.base/share/classes/java/security/{DEREncodable.java => BinaryEncodable.java} (79%) create mode 100644 src/java.base/share/classes/javax/crypto/CryptoException.java diff --git a/src/java.base/share/classes/java/security/AsymmetricKey.java b/src/java.base/share/classes/java/security/AsymmetricKey.java index d37afe9bfea..2177332d174 100644 --- a/src/java.base/share/classes/java/security/AsymmetricKey.java +++ b/src/java.base/share/classes/java/security/AsymmetricKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,7 @@ import java.security.spec.AlgorithmParameterSpec; * * @since 22 */ -public non-sealed interface AsymmetricKey extends Key, DEREncodable { +public non-sealed interface AsymmetricKey extends Key, BinaryEncodable { /** * Returns the parameters associated with this key. * The parameters are optional and may be either diff --git a/src/java.base/share/classes/java/security/DEREncodable.java b/src/java.base/share/classes/java/security/BinaryEncodable.java similarity index 79% rename from src/java.base/share/classes/java/security/DEREncodable.java rename to src/java.base/share/classes/java/security/BinaryEncodable.java index 1401336037c..bd5d05ee4ec 100644 --- a/src/java.base/share/classes/java/security/DEREncodable.java +++ b/src/java.base/share/classes/java/security/BinaryEncodable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,20 +25,22 @@ package java.security; -import jdk.internal.javac.PreviewFeature; - import javax.crypto.EncryptedPrivateKeyInfo; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import jdk.internal.javac.PreviewFeature; + /** * This interface is implemented by security API classes that contain - * binary-encodable key or certificate material. - * These APIs or their subclasses typically provide methods to convert - * their instances to and from byte arrays in the Distinguished - * Encoding Rules (DER) format. + * binary-encodable cryptographic material. + * + *

This sealed interface may evolve. When using {@code switch}, always include a + * {@code default} case rather than relying on the classes specified in the + * {@code permits} clause to remain fixed. An exhaustive {@code switch} may + * result in a {@link MatchException}. * * @see AsymmetricKey * @see KeyPair @@ -49,11 +51,11 @@ import java.security.spec.X509EncodedKeySpec; * @see X509CRL * @see PEM * - * @since 25 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) -public sealed interface DEREncodable permits AsymmetricKey, KeyPair, +public sealed interface BinaryEncodable permits AsymmetricKey, KeyPair, PKCS8EncodedKeySpec, X509EncodedKeySpec, EncryptedPrivateKeyInfo, X509Certificate, X509CRL, PEM { } diff --git a/src/java.base/share/classes/java/security/KeyPair.java b/src/java.base/share/classes/java/security/KeyPair.java index 39c98501fea..0efd134c93c 100644 --- a/src/java.base/share/classes/java/security/KeyPair.java +++ b/src/java.base/share/classes/java/security/KeyPair.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,7 +37,7 @@ package java.security; * @since 1.1 */ -public final class KeyPair implements java.io.Serializable, DEREncodable { +public final class KeyPair implements java.io.Serializable, BinaryEncodable { @java.io.Serial private static final long serialVersionUID = -7565189502268009837L; diff --git a/src/java.base/share/classes/java/security/PEM.java b/src/java.base/share/classes/java/security/PEM.java index 2068fb707dc..13ee9f107cf 100644 --- a/src/java.base/share/classes/java/security/PEM.java +++ b/src/java.base/share/classes/java/security/PEM.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,53 +27,43 @@ package java.security; import jdk.internal.javac.PreviewFeature; +import jdk.internal.ref.CleanerFactory; +import sun.security.util.KeyUtil; import sun.security.util.Pem; import java.io.InputStream; +import java.nio.charset.StandardCharsets; 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. + * A {@link BinaryEncodable} representing a Privacy-Enhanced Mail (PEM) structure + * composed of a type identifier, Base64-encoded content, and optional + * leading data that precedes the PEM header. * - *

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: + *

The {@code type} is the label in the PEM header, following the + * {@code BEGIN} keyword and excluding the encapsulation boundaries. + * 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. + *

Instances of this class are returned by {@link PEMDecoder#decode(String)} + * and {@link PEMDecoder#decode(InputStream)} when the content cannot be represented + * as a cryptographic object. To explicitly retrieve a {@code PEM} instance + * with access to the leading data, use {@link PEMDecoder#decode(String, Class)} + * or {@link PEMDecoder#decode(InputStream, Class)} with {@code PEM.class} as the + * type. * - * @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}. + *

A {@code PEM} object can be encoded to its textual representation by + * invoking {@link #toString()} or by using {@link PEMEncoder}. + * + *

To construct a {@code PEM} instance, {@code type} and + * {@code base64Content} must be non-{@code null}. For constructors that accept + * {@code leadingData}, it must also be non-{@code null}. + * + *

No validation is performed to ensure that the {@code type} conforms to + * RFC 7468 or legacy formats, or that the content corresponds to the declared + * {@code type}. * * @spec https://www.rfc-editor.org/info/rfc7468 * RFC 7468: Textual Encodings of PKIX, PKCS, and CMS Structures @@ -84,64 +74,168 @@ import java.util.Objects; * @since 26 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) -public record PEM(String type, String content, byte[] leadingData) - implements DEREncodable { +public final class PEM implements BinaryEncodable { + + private final String type; + private final byte[] content; + private byte[] leadingData; /** - * Creates a {@code PEM} instance with the specified parameters. + * Creates a {@code PEM} instance with the specified type, Base64-encoded + * content string, and leading data byte array. * - * @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} + * @param type the PEM type identifier; must not contain PEM encapsulation + * syntax + * @param base64Content the Base64-encoded content, excluding the PEM header + * and footer + * @param leadingData data that precedes the PEM header. + * This array is defensively copied. + * + * @throws IllegalArgumentException if {@code type} contains PEM + * encapsulation syntax + * @throws NullPointerException if any parameter is {@code null} */ - public PEM { - Objects.requireNonNull(type, "\"type\" cannot be null."); - Objects.requireNonNull(content, "\"content\" cannot be null."); + public PEM(String type, String base64Content, byte[] leadingData) { + Objects.requireNonNull(base64Content, "base64Content cannot be null"); + this(type, base64Content.getBytes(StandardCharsets.ISO_8859_1), + leadingData); + } - // With no validity checking on `type`, the constructor accept anything - // including lowercase. The onus is on the caller. + /** + * Creates a {@code PEM} instance with the specified type and Base64-encoded + * content string. + * + * @param type the PEM type identifier; must not contain PEM encapsulation + * syntax + * @param base64Content the Base64-encoded content, excluding the PEM header + * and footer + * @throws IllegalArgumentException if {@code type} contains PEM + * encapsulation syntax + * @throws NullPointerException if any parameter is {@code null} + */ + public PEM(String type, String base64Content) { + Objects.requireNonNull(base64Content, "base64Content cannot be null"); + this(type, base64Content.getBytes(StandardCharsets.ISO_8859_1)); + } + + /** + * Creates a {@code PEM} instance with the specified type and Base64-encoded + * content and leading data as byte arrays. + * + * @param type the PEM type identifier; must not contain PEM encapsulation + * syntax + * @param base64Content the Base64-encoded content, excluding the PEM header + * and footer. This array is defensively copied. + * @param leadingData data that precedes the PEM header. + * This array is defensively copied. + * + * @throws IllegalArgumentException if {@code type} contains PEM + * encapsulation syntax + * @throws NullPointerException if any parameter is {@code null} + * + * @since 27 + */ + public PEM(String type, byte[] base64Content, byte[] leadingData) { + this(type, base64Content); + this.leadingData = Objects.requireNonNull( + leadingData, "leadingData cannot be null").clone(); + } + + /** + * Creates a {@code PEM} instance with the specified type and Base64-encoded + * content byte array. + * + * @param type the PEM type identifier; must not contain PEM encapsulation + * syntax + * @param base64Content the Base64-encoded content, excluding the PEM header + * and footer. This array is defensively copied. + * @throws IllegalArgumentException if {@code type} contains PEM + * encapsulation syntax + * @throws NullPointerException if any parameter is {@code null} + * + * @since 27 + */ + public PEM(String type, byte[] base64Content) { + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(base64Content, "base64Content cannot be null"); + + // The `type` is not checked against any specification. The onus is on + // the caller. Only minor formatting checks are done if (type.startsWith("-") || type.startsWith("BEGIN ") || type.startsWith("END ")) { throw new IllegalArgumentException("PEM syntax labels found. " + "Only the PEM type identifier is allowed."); } + + content = base64Content.clone(); + this.type = type; + final var c = content; + CleanerFactory.cleaner().register(this, () -> KeyUtil.clear(c)); } /** - * Creates a {@code PEM} instance with the specified type and content. This - * constructor sets {@code leadingData} to {@code null}. + * Returns the PEM type identifier. * - * @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} + * @return the PEM type identifier */ - public PEM(String type, String content) { - this(type, content, null); + public String type() { + return type; } /** - * Returns the PEM formatted string containing the {@code type} and - * Base64-encoded {@code content}. {@code leadingData} is not included. + * Returns the leading data that preceded the PEM header in the decoded + * input. * - * @return the PEM text representation + * @return a newly-allocated byte array containing leading data, or + * {@code null} if no leading data is present */ - @Override - final public String toString() { - return Pem.pemEncoded(this); + public byte[] leadingData() { + return (leadingData != null) ? leadingData.clone() : null; } /** - * Returns a Base64-decoded byte array of {@code content}, using + * Returns the Base64-encoded content. + * + * @return a newly-allocated byte array containing the Base64 content + * + * @since 27 + */ + public byte[] content() { + return content.clone(); + } + + /** + * Returns the Base64-decoded content as a byte array, using * {@link Base64#getMimeDecoder()}. * - * @return a decoded byte array + * @return a newly-allocated byte array containing the decoded content * @throws IllegalArgumentException if decoding fails */ - final public byte[] decode() { + public byte[] decode() { return Base64.getMimeDecoder().decode(content); } + + /** + * Returns a PEM string representation of this object, using {@code type} + * for the header and footer lines and {@code content} for the Base64 body. + * + * @return the PEM-formatted string + */ + @Override + public String toString() { + return new String(Pem.pemEncoded(type, content), + StandardCharsets.ISO_8859_1); + } + + /* + * Returns the PEM string representation as a byte array. + */ + byte[] toTextualByteArray() { + return Pem.pemEncoded(type, content); + } + + // Clear internal content + void clear() { + KeyUtil.clear(content); + } } diff --git a/src/java.base/share/classes/java/security/PEMDecoder.java b/src/java.base/share/classes/java/security/PEMDecoder.java index 7a4b7753876..f5a6a70d0f5 100644 --- a/src/java.base/share/classes/java/security/PEMDecoder.java +++ b/src/java.base/share/classes/java/security/PEMDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,87 +34,85 @@ import sun.security.util.KeyUtil; import sun.security.util.Pem; import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.CryptoException; import javax.crypto.spec.PBEKeySpec; import java.io.*; import java.lang.ref.Reference; import java.nio.charset.StandardCharsets; import java.security.cert.*; import java.security.spec.*; -import java.util.Base64; import java.util.Objects; /** * {@code PEMDecoder} implements a decoder for Privacy-Enhanced Mail (PEM) data. * PEM is a textual encoding used to store and transfer cryptographic * objects, such as asymmetric keys, certificates, and certificate revocation - * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of a - * Base64-encoded binary encoding enclosed by a type-identifying header + * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of + * Base64-encoded content enclosed by a type-identifying header * and footer. * *

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

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

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 - * {@code PEM} object. + *

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

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

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

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

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

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

This class is immutable and thread-safe. * @@ -127,14 +125,13 @@ import java.util.Objects; *

Example: configure decryption and a factory provider: * {@snippet lang = java: * PEMDecoder pd = PEMDecoder.of().withDecryption(password). - * withFactory(provider); - * DEREncodable pemData = pd.decode(privKeyPEM); - * } + * withFactoriesOf(provider); + * BinaryEncodable pemData = pd.decode(privKeyPEM); + *} * - * @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. + * @implNote This implementation decodes non-encrypted RSA PRIVATE KEY as {@code PrivateKey}, + * X509 CERTIFICATE and X.509 CERTIFICATE as {@code X509Certificate}, and CRL as + * {@code X509CRL}. Other implementations may recognize additional PEM types. * * @see PEMEncoder * @see PEM @@ -149,7 +146,6 @@ import java.util.Objects; * * @since 25 */ - @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public final class PEMDecoder { private final Provider factory; @@ -159,24 +155,23 @@ public final class PEMDecoder { private final static PEMDecoder PEM_DECODER = new PEMDecoder(null, null); /** - * Creates an instance with a specific KeyFactory and/or password. - * @param withFactory KeyFactory provider - * @param withPassword char[] password for EncryptedPrivateKeyInfo - * decryption + * Creates an instance with a specific provider and/or password. + * @param withFactory Key/Certificate factory provider + * @param withKeySpec PBEKeySpec for EncryptedPrivateKeyInfo decryption */ - private PEMDecoder(Provider withFactory, PBEKeySpec withPassword) { - keySpec = withPassword; + private PEMDecoder(Provider withFactory, PBEKeySpec withKeySpec) { + keySpec = withKeySpec; factory = withFactory; - if (withPassword != null) { + if (withKeySpec != null) { final var k = this.keySpec; CleanerFactory.cleaner().register(this, k::clearPassword); } } /** - * Returns an instance of {@code PEMDecoder}. + * Returns the default {@code PEMDecoder} instance. * - * @return a {@code PEMDecoder} instance + * @return the default {@code PEMDecoder} */ public static PEMDecoder of() { return PEM_DECODER; @@ -187,23 +182,21 @@ public final class PEMDecoder { * header and footer and proceed with decoding the base64 for the * appropriate type. */ - private DEREncodable decode(PEM pem) { - Base64.Decoder decoder = Base64.getMimeDecoder(); - + private BinaryEncodable decode(PEM pem) { try { return switch (pem.type()) { case Pem.PUBLIC_KEY -> { X509EncodedKeySpec spec = - new X509EncodedKeySpec(decoder.decode(pem.content())); + new X509EncodedKeySpec(pem.decode()); yield getKeyFactory( KeyUtil.getAlgorithm(spec.getEncoded())). generatePublic(spec); } case Pem.PRIVATE_KEY -> { - DEREncodable d; + BinaryEncodable d; PKCS8Key p8key = null; PKCS8EncodedKeySpec p8spec = null; - byte[] encoding = decoder.decode(pem.content()); + byte[] encoding = pem.decode(); try { p8key = new PKCS8Key(encoding); @@ -238,36 +231,37 @@ public final class PEMDecoder { } case Pem.ENCRYPTED_PRIVATE_KEY -> { byte[] p8 = null; - byte[] encoding = null; + var ekpi = new EncryptedPrivateKeyInfo(pem.decode()); + if (keySpec == null) { + yield ekpi; + } 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); + } catch (GeneralSecurityException e) { + throw new CryptoException(e); + } + try { + yield Pem.toPKCS8Encodable(p8, factory); } finally { Reference.reachabilityFence(this); - KeyUtil.clear(encoding, p8); + KeyUtil.clear(p8); } } case Pem.CERTIFICATE, Pem.X509_CERTIFICATE, Pem.X_509_CERTIFICATE -> { CertificateFactory cf = getCertFactory("X509"); yield (X509Certificate) cf.generateCertificate( - new ByteArrayInputStream(decoder.decode(pem.content()))); + new ByteArrayInputStream(pem.decode())); } case Pem.X509_CRL, Pem.CRL -> { CertificateFactory cf = getCertFactory("X509"); yield (X509CRL) cf.generateCRL( - new ByteArrayInputStream(decoder.decode(pem.content()))); + new ByteArrayInputStream(pem.decode())); } case Pem.RSA_PRIVATE_KEY -> { KeyFactory kf = getKeyFactory("RSA"); yield kf.generatePrivate( - RSAPrivateCrtKeyImpl.getKeySpec(decoder.decode( - pem.content()))); + RSAPrivateCrtKeyImpl.getKeySpec(pem.decode())); } default -> pem; }; @@ -277,155 +271,182 @@ public final class PEMDecoder { } /** - * Decodes and returns a {@code DEREncodable} from the given {@code String}. + * Decodes and returns a {@code BinaryEncodable} from the given {@code String}. * *

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

A {@code DEREncodable} will be returned that best represents the - * decoded data. If the PEM type is not supported, a {@code PEM} object is + *

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

Input consumed by this method is read in as + *

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

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

A {@code DEREncodable} will be returned that best represents the - * decoded data. If the PEM type is not supported, a {@code PEM} object is + *

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

If no PEM data is found, an {@code EOFException} is thrown. * * @param is {@code InputStream} containing PEM data - * @return a {@code DEREncodable} - * @throws IOException on IO or PEM syntax error where the - * {@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 {@code null} + * @return a {@code BinaryEncodable} + * @throws IOException if an I/O error occurs or PEM syntax is invalid + * @throws EOFException if no PEM data is found or the stream ends unexpectedly + * @throws IllegalArgumentException if decoding fails + * @throws NullPointerException if {@code InputStream} is {@code null} + * @throws CryptoException if an error occurs during decryption + * + * @since 27 */ - public DEREncodable decode(InputStream is) throws IOException { + public BinaryEncodable decode(InputStream is) throws IOException { Objects.requireNonNull(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 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 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 {@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} 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); + BinaryEncodable be = null; try { - return decode(new ByteArrayInputStream( - str.getBytes(StandardCharsets.UTF_8)), tClass); - } catch (IOException e) { - // With all data contained in the String, there are no IO ops. - throw new IllegalArgumentException(e); + be = decode(pem); + return be; + } finally { + if (be != pem) { + pem.clear(); + } } } /** - * 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. + * Decodes and returns a {@code BinaryEncodable} of the specified class from + * the given PEM string. + * + *

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

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

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

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

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

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

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 {@code tClass} is {@code PEM.class}, a {@code PEM} object is returned + * containing the type identifier, Base64-encoded data, and any leading data + * preceding the PEM header. For {@code BinaryEncodable} types other than + * {@code PEM}, leading data is ignored. * *

If no PEM data is found, an {@code EOFException} is thrown. * - * @param class type parameter that extends {@code DEREncodable} + * @param class type parameter that extends {@code BinaryEncodable} * @param is an {@code InputStream} containing PEM data * @param tClass the returned object class that extends or implements - * {@code 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 no PEM data found or unexpectedly reached the - * end of the {@code InputStream} - * @throws IllegalArgumentException on error in decoding + * {@code BinaryEncodable} + * @return a {@code BinaryEncodable} of type {@code tClass} + * @throws IOException if an I/O error occurs or PEM syntax is invalid + * @throws EOFException if no PEM data is found or the stream ends unexpectedly + * @throws IllegalArgumentException if decoding fails * @throws ClassCastException if {@code tClass} does not represent the PEM type - * @throws NullPointerException when any input values are {@code null} + * @throws NullPointerException if any input values are {@code null} + * @throws CryptoException if an error occurs during decryption * - * @see #decode(InputStream) + * @see #decode(InputStream) * @see #decode(String, Class) + * + * @since 27 */ - public S decode(InputStream is, Class tClass) + public S decode(InputStream is, Class tClass) throws IOException { Objects.requireNonNull(is); Objects.requireNonNull(tClass); PEM pem = Pem.readPEM(is); - if (tClass.isAssignableFrom(PEM.class)) { + if (tClass == PEM.class) { return tClass.cast(pem); + } else if (tClass == BinaryEncodable.class) { + pem.clear(); + throw new ClassCastException("BinaryEncodable is not a PEM type"); + } + + BinaryEncodable so; + try { + so = decode(pem); + } finally { + pem.clear(); } - DEREncodable so = decode(pem); /* * If the object is a KeyPair, check if the tClass is set to class @@ -441,6 +462,9 @@ public final class PEMDecoder { if ((PublicKey.class).isAssignableFrom(tClass) || (X509EncodedKeySpec.class).isAssignableFrom(tClass)) { so = kp.getPublic(); + if (kp.getPrivate() instanceof PKCS8Key p8Key) { + KeyUtil.clear(p8Key); + } } } @@ -470,6 +494,10 @@ public final class PEMDecoder { throw new ClassCastException("Invalid KeySpec " + "specified: " + tClass.getName() + " for key " + key.getClass().getName()); + } finally { + if (key instanceof PKCS8Key p8Key) { + KeyUtil.clear(p8Key); + } } } @@ -509,19 +537,29 @@ public final class PEMDecoder { * from the specified {@code Provider} to produce cryptographic objects. * Any errors using the {@code Provider} will occur during decoding. * - * @param provider the factory provider + * @param provider the factory {@code Provider} * @return a new {@code PEMDecoder} instance configured with the {@code Provider} * @throws NullPointerException if {@code provider} is {@code null} + * + * @since 27 */ - public PEMDecoder withFactory(Provider provider) { + public PEMDecoder withFactoriesOf(Provider provider) { Objects.requireNonNull(provider); - return new PEMDecoder(provider, keySpec); + if (keySpec == null) { + return new PEMDecoder(provider, null); + } + char[] passwd = keySpec.getPassword(); + try { + return new PEMDecoder(provider, new PBEKeySpec(passwd)); + } finally { + KeyUtil.clear(passwd); + } } /** * Returns a copy of this {@code PEMDecoder} that decodes and decrypts * encrypted private keys using the specified password. - * Non-encrypted PEM can also be decoded from this instance. + * Unencrypted PEM can also be decoded by the returned instance. * * @param password the password to decrypt the encrypted PEM data. This array * is cloned and stored in the new instance. diff --git a/src/java.base/share/classes/java/security/PEMEncoder.java b/src/java.base/share/classes/java/security/PEMEncoder.java index 62a0942caf7..211b47008a5 100644 --- a/src/java.base/share/classes/java/security/PEMEncoder.java +++ b/src/java.base/share/classes/java/security/PEMEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,8 @@ package java.security; import jdk.internal.javac.PreviewFeature; + +import jdk.internal.ref.CleanerFactory; import sun.security.pkcs.PKCS8Key; import sun.security.util.KeyUtil; import sun.security.util.Pem; @@ -35,7 +37,6 @@ import javax.crypto.spec.PBEKeySpec; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.cert.*; -import java.security.spec.AlgorithmParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Objects; @@ -44,22 +45,19 @@ import java.util.Objects; * {@code PEMEncoder} implements an encoder for Privacy-Enhanced Mail (PEM) * data. PEM is a textual encoding used to store and transfer cryptographic * objects, such as asymmetric keys, certificates, and certificate revocation - * lists (CRLs). It is defined in RFC 1421 and RFC 7468. PEM consists of a - * Base64-encoded 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 content enclosed by a type-identifying header * and footer. * *

Encoding can be performed on cryptographic objects that - * implement {@link DEREncodable}. The {@link #encode(DEREncodable)} - * and {@link #encodeToString(DEREncodable)} methods encode a {@code DEREncodable} + * implement {@link BinaryEncodable}. The {@link #encode(BinaryEncodable)} + * and {@link #encodeToString(BinaryEncodable)} methods encode a {@code BinaryEncodable} * 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 {@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 {@link EncryptedPrivateKeyInfo} object can be encoded - * directly to PEM by passing it to the {@code encode} or - * {@code encodeToString} methods. + * configured to encrypt the key with that password. * *

PKCS #8 v2.0 defines the ASN.1 OneAsymmetricKey structure, which may * contain both private and public keys. @@ -72,24 +70,24 @@ import java.util.Objects; * {@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: + *

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

*

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

    - *
  • {@link PrivateKey} : ENCRYPTED PRIVATE KEY
  • - *
  • {@link KeyPair} : ENCRYPTED PRIVATE KEY
  • - *
  • {@link PKCS8EncodedKeySpec} : ENCRYPTED PRIVATE KEY
  • + *
  • {@link PrivateKey}: ENCRYPTED PRIVATE KEY
  • + *
  • {@link KeyPair}: ENCRYPTED PRIVATE KEY
  • + *
  • {@link PKCS8EncodedKeySpec}: ENCRYPTED PRIVATE KEY
  • *
* *

This class is immutable and thread-safe. @@ -108,7 +106,6 @@ import java.util.Objects; * * @implNote Implementations may support additional PEM types. * - * * @see PEMDecoder * @see PEM * @see EncryptedPrivateKeyInfo @@ -128,21 +125,24 @@ public final class PEMEncoder { // Singleton instance of PEMEncoder private static final PEMEncoder PEM_ENCODER = new PEMEncoder(null); // PBE key for encryption - private final Key key; + private final SecretKey key; /** - * Create an encrypted {@code PEMEncoder} instance. + * Creates a PEMEncoder instance configured for the given keySpec. */ private PEMEncoder(PBEKeySpec keySpec) { if (keySpec != null) { try { key = SecretKeyFactory.getInstance(Pem.DEFAULT_ALGO). generateSecret(keySpec); + final SecretKey k = this.key; + CleanerFactory.cleaner().register(this, + () -> KeyUtil.destroySecretKeys(k)); } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("Operation failed: " + + throw new CryptoException("Operation failed: " + "unable to generate key or locate a valid algorithm. " + "Check the jdk.epkcs8.defaultAlgorithm security " + - "property for a valid configuration.", e); + "property for a valid configuration", e); } } else { key = null; @@ -159,63 +159,85 @@ public final class PEMEncoder { } /** - * Encodes the specified {@code DEREncodable} and returns a PEM-encoded + * Encodes the specified {@code BinaryEncodable} and returns a PEM-encoded * string. * - * @param de the {@code DEREncodable} to be encoded + * @param be the {@code BinaryEncodable} to encode * @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} + * @throws IllegalArgumentException if {@code be} has no encoding, is + * an unsupported class, or cannot be used with encryption + * @throws NullPointerException if {@code be} is {@code null} + * @throws CryptoException if an error occurs during encryption * @see #withEncryption(char[]) + * + * @since 27 */ - public String encodeToString(DEREncodable de) { - Objects.requireNonNull(de); - return switch (de) { - case PublicKey pu -> buildKey(pu.getEncoded(), null); - case PrivateKey pr -> { - byte[] encoding = pr.getEncoded(); - try { - yield buildKey(null, encoding); - } finally { - KeyUtil.clear(encoding); - } + public String encodeToString(BinaryEncodable be) { + Objects.requireNonNull(be); + if (be instanceof PEM pem) { + if (key != null) { + throw new IllegalArgumentException("PEM cannot be " + + "encrypted"); } + return pem.toString(); + } + return KeyUtil.clear(encode(be), + e -> new String(e, StandardCharsets.ISO_8859_1)); + } + + /** + * Encodes the specified {@code BinaryEncodable} and returns a PEM-encoded + * byte array. + * + * @param be the {@code BinaryEncodable} to encode + * @return a PEM-encoded byte array + * @throws IllegalArgumentException if {@code be} has no encoding, is + * an unsupported class, or cannot be used with encryption + * @throws NullPointerException if {@code be} is {@code null} + * @throws CryptoException if an error occurs during encryption + * @see #withEncryption(char[]) + * + * @since 27 + */ + public byte[] encode(BinaryEncodable be) { + return switch (be) { + case PublicKey pu -> buildKey(pu.getEncoded(), null); + case PrivateKey pr -> + KeyUtil.clear(pr.getEncoded(), e -> buildKey(null, e)); 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); + if (kp.getPublic() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PublicKey"); } + if (kp.getPrivate() == null) { + throw new IllegalArgumentException("KeyPair does not " + + "contain PrivateKey"); + } + byte[] pubEncoding = kp.getPublic().getEncoded(); + if (pubEncoding == null || pubEncoding.length == 0) { + throw new IllegalArgumentException("PublicKey is " + + "null or has no encoding"); + } + byte[] encoding = kp.getPrivate().getEncoded(); + if (encoding == null || encoding.length == 0) { + throw new IllegalArgumentException("PrivateKey is " + + "null or has no encoding"); + } + yield KeyUtil.clear(encoding, e -> buildKey(pubEncoding, e)); } case X509EncodedKeySpec x -> buildKey(x.getEncoded(), null); - case PKCS8EncodedKeySpec p -> buildKey(null, p.getEncoded()); + case PKCS8EncodedKeySpec p -> + KeyUtil.clear(p.getEncoded(), e -> buildKey(null, e)); case EncryptedPrivateKeyInfo epki -> { - byte[] encoding = null; if (key != null) { throw new IllegalArgumentException( "EncryptedPrivateKeyInfo cannot be encrypted"); } try { - encoding = epki.getEncoded(); - yield Pem.pemEncoded(Pem.ENCRYPTED_PRIVATE_KEY, encoding); + yield KeyUtil.clear(epki.getEncoded(), + e -> Pem.pemEncodedFromDER(Pem.ENCRYPTED_PRIVATE_KEY, e)); } catch (IOException e) { throw new IllegalArgumentException(e); - } finally { - KeyUtil.clear(encoding); } } case X509Certificate c -> { @@ -224,7 +246,7 @@ public final class PEMEncoder { "cannot be encrypted"); } try { - yield Pem.pemEncoded(Pem.CERTIFICATE, c.getEncoded()); + yield Pem.pemEncodedFromDER(Pem.CERTIFICATE, c.getEncoded()); } catch (CertificateEncodingException e) { throw new IllegalArgumentException(e); } @@ -235,7 +257,7 @@ public final class PEMEncoder { "encrypted"); } try { - yield Pem.pemEncoded(Pem.X509_CRL, crl.getEncoded()); + yield Pem.pemEncodedFromDER(Pem.X509_CRL, crl.getEncoded()); } catch (CRLException e) { throw new IllegalArgumentException(e); } @@ -245,53 +267,39 @@ public final class PEMEncoder { throw new IllegalArgumentException("PEM cannot be " + "encrypted"); } - yield Pem.pemEncoded(rec); + yield rec.toTextualByteArray(); } default -> throw new IllegalArgumentException("PEM does not " + - "support " + de.getClass().getCanonicalName()); + "support " + be.getClass().getCanonicalName()); }; } /** - * 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 - * @throws NullPointerException if {@code de} is {@code null} - * @see #withEncryption(char[]) - */ - public byte[] encode(DEREncodable de) { - return encodeToString(de).getBytes(StandardCharsets.ISO_8859_1); - } - - /** - * Returns a copy of this PEMEncoder that encrypts and encodes - * using the specified password and default encryption algorithm. + * Returns a copy of this {@code PEMEncoder} configured to encrypt and + * encode using the specified password and the default encryption algorithm. * *

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}. + * configured instance. Attempting to encode other {@code BinaryEncodable} + * objects will throw an {@code IllegalArgumentException}. + * + *

To use non-default encryption parameters or a different provider, use + * an {@code encrypt} method in {@link EncryptedPrivateKeyInfo}, then pass + * the resulting object to {@link #encode(BinaryEncodable)}. * * @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)}. + * defaults are determined by the provider. * * @param password the encryption password. The array is cloned and * stored in the new instance. * @return a new {@code PEMEncoder} instance configured for encryption - * @throws NullPointerException if password is {@code null} - * @throws IllegalArgumentException if generating the encryption key fails + * @throws NullPointerException if {@code password} is {@code null} + * @throws CryptoException if generating the encryption key fails */ public PEMEncoder withEncryption(char[] password) { - Objects.requireNonNull(password, "password cannot be null."); + Objects.requireNonNull(password, "password cannot be null"); PBEKeySpec keySpec = new PBEKeySpec(password); try { return new PEMEncoder(keySpec); @@ -301,14 +309,12 @@ public final class PEMEncoder { } /** - * Build PEM encoding. - * - * privateKeyEncoding will be zeroed when the method returns + * Build the PEM encoding for AsymmetricKey and KeyPair */ - private String buildKey(byte[] publicEncoding, byte[] privateEncoding) { + private byte[] buildKey(byte[] publicEncoding, byte[] privateEncoding) { if (publicEncoding == null && privateEncoding == null) { throw new IllegalArgumentException("No encoded data given by the " + - "DEREncodable."); + "BinaryEncodable"); } if (publicEncoding != null && publicEncoding.length == 0) { @@ -322,35 +328,36 @@ public final class PEMEncoder { } if (key != null && privateEncoding == null) { - throw new IllegalArgumentException("This DEREncodable cannot " + - "be encrypted."); + throw new IllegalArgumentException("This BinaryEncodable cannot " + + "be encrypted"); } // X509 only if (publicEncoding != null && privateEncoding == null) { - return Pem.pemEncoded(Pem.PUBLIC_KEY, publicEncoding); + return Pem.pemEncodedFromDER(Pem.PUBLIC_KEY, publicEncoding); } byte[] encoding = null; PKCS8EncodedKeySpec p8KeySpec = null; try { if (publicEncoding == null) { - encoding = privateEncoding; + encoding = privateEncoding.clone(); } else { encoding = PKCS8Key.getEncoded(publicEncoding, privateEncoding); } if (key != null) { p8KeySpec = new PKCS8EncodedKeySpec(encoding); + KeyUtil.clear(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."); + "given by the BinaryEncodable"); } - return Pem.pemEncoded( + return Pem.pemEncodedFromDER( (key == null ? Pem.PRIVATE_KEY : Pem.ENCRYPTED_PRIVATE_KEY), encoding); } catch (IOException e) { diff --git a/src/java.base/share/classes/java/security/cert/X509CRL.java b/src/java.base/share/classes/java/security/cert/X509CRL.java index d19618f81ed..2745f0e377b 100644 --- a/src/java.base/share/classes/java/security/cert/X509CRL.java +++ b/src/java.base/share/classes/java/security/cert/X509CRL.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -107,7 +107,7 @@ import java.util.Set; * @see X509Extension */ -public abstract non-sealed class X509CRL extends CRL implements X509Extension, DEREncodable { +public abstract non-sealed class X509CRL extends CRL implements X509Extension, BinaryEncodable { private transient X500Principal issuerPrincipal; diff --git a/src/java.base/share/classes/java/security/cert/X509Certificate.java b/src/java.base/share/classes/java/security/cert/X509Certificate.java index fe4a472dead..bd19f3d33d0 100644 --- a/src/java.base/share/classes/java/security/cert/X509Certificate.java +++ b/src/java.base/share/classes/java/security/cert/X509Certificate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -108,7 +108,7 @@ import java.util.List; */ public abstract non-sealed class X509Certificate extends Certificate - implements X509Extension, DEREncodable { + implements X509Extension, BinaryEncodable { @java.io.Serial private static final long serialVersionUID = -2491127588187038216L; diff --git a/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java b/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java index 0518e6ed272..319f1ad4cc8 100644 --- a/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java +++ b/src/java.base/share/classes/java/security/spec/PKCS8EncodedKeySpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ package java.security.spec; -import java.security.DEREncodable; +import java.security.BinaryEncodable; /** * This class represents the ASN.1 encoding of a private key, @@ -73,7 +73,7 @@ import java.security.DEREncodable; */ public non-sealed class PKCS8EncodedKeySpec extends EncodedKeySpec implements - DEREncodable { + BinaryEncodable { /** * Creates a new {@code PKCS8EncodedKeySpec} with the given encoded key. * diff --git a/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java b/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java index 6d0f105e64f..875660a8ed4 100644 --- a/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java +++ b/src/java.base/share/classes/java/security/spec/X509EncodedKeySpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,7 +25,7 @@ package java.security.spec; -import java.security.DEREncodable; +import java.security.BinaryEncodable; /** * This class represents the ASN.1 encoding of a public key, @@ -52,7 +52,7 @@ import java.security.DEREncodable; */ public non-sealed class X509EncodedKeySpec extends EncodedKeySpec implements - DEREncodable { + BinaryEncodable { /** * Creates a new {@code X509EncodedKeySpec} with the given encoded key. * diff --git a/src/java.base/share/classes/javax/crypto/CryptoException.java b/src/java.base/share/classes/javax/crypto/CryptoException.java new file mode 100644 index 00000000000..367417abf9d --- /dev/null +++ b/src/java.base/share/classes/javax/crypto/CryptoException.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javax.crypto; + +import jdk.internal.javac.PreviewFeature; + +/** + * Thrown to indicate a cryptographic failure during processing. + * + *

This exception represents a general cryptographic error. It is typically + * used for unrecoverable failures related to + * {@link java.security.GeneralSecurityException} in contexts where checked + * exceptions are not desired. + * + *

This exception is not intended to represent internal provider errors, + * which should be reported using {@link java.security.ProviderException}. + * + * @since 27 + */ +@PreviewFeature(feature = PreviewFeature.Feature.PEM_API) +public final class CryptoException extends RuntimeException { + + @java.io.Serial + private static final long serialVersionUID = -6824337376392797817L; + + /** + * Constructs a new {@code CryptoException} with {@code null} as its detail + * message. The cause is not initialized and may subsequently be initialized + * by a call to {@link #initCause(Throwable)}. + */ + public CryptoException() { + super(); + } + + /** + * Constructs a new {@code CryptoException} with the specified detail message. + * The cause is not initialized and may subsequently be initialized by a + * call to {@link #initCause(Throwable)}. + * + * @param message the detail message. The detail message is saved for later + * retrieval by the {@link #getMessage()} method. + */ + public CryptoException(String message) { + super(message); + } + + /** + * Constructs a new {@code CryptoException} with the specified detail + * message and cause. + * + *

Note that the detail message associated with {@code cause} is not + * automatically incorporated in this exception's detail message. + * + * @param message the detail message. The detail message is saved for later + * retrieval by the {@link #getMessage()} method. + * @param cause the cause. The cause is saved for later retrieval by the + * {@link #getCause()} method. A {@code null} value is permitted + * and indicates that the cause is nonexistent or unknown. + */ + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new {@code CryptoException} with the specified cause and a detail + * message of {@code (cause == null ? null : cause.toString())}, which + * typically contains the class and detail message of {@code cause}. + * + * @param cause the cause. The cause is saved for later retrieval by the + * {@link #getCause()} method. A {@code null} value is permitted + * and indicates that the cause is nonexistent or unknown. + */ + public CryptoException(Throwable cause) { + super(cause); + } +} diff --git a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java index dc3c4c14b27..632c81eab16 100644 --- a/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java +++ b/src/java.base/share/classes/javax/crypto/EncryptedPrivateKeyInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -60,7 +60,7 @@ import java.util.Objects; * @since 1.4 */ -public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { +public non-sealed class EncryptedPrivateKeyInfo implements BinaryEncodable { // The "encryptionAlgorithm" is stored in either the algid or // the params field. Precisely, if this object is created by @@ -221,7 +221,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } /** - * Create an EncryptedPrivateKeyInfo object from the given components + * Create an EncryptedPrivateKeyInfo object from the given components. */ private EncryptedPrivateKeyInfo(byte[] encoded, byte[] eData, AlgorithmId id, AlgorithmParameters p) { @@ -265,8 +265,8 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } /** - * Extract the enclosed PKCS8EncodedKeySpec object from the - * encrypted data and return it. + * Extracts the enclosed PKCS8EncodedKeySpec object from the + * encrypted data and returns it. *
Note: In order to successfully retrieve the enclosed * PKCS8EncodedKeySpec object, {@code cipher} needs * to be initialized to either Cipher.DECRYPT_MODE or @@ -275,7 +275,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { * * @param cipher the initialized {@code Cipher} object which will be * used for decrypting the encrypted data. - * @return the PKCS8EncodedKeySpec object. + * @return the PKCS8EncodedKeySpec object * @exception NullPointerException if {@code cipher} is {@code null}. * @exception InvalidKeySpecException if the given cipher is * inappropriate for the encrypted data or the encrypted @@ -283,7 +283,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { */ public PKCS8EncodedKeySpec getKeySpec(Cipher cipher) throws InvalidKeySpecException { - byte[] encoded; + byte[] encoded = null; try { encoded = cipher.doFinal(encryptedData); return pkcs8EncodingToSpec(encoded); @@ -292,6 +292,8 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { IllegalStateException ex) { throw new InvalidKeySpecException( "Cannot retrieve the PKCS8EncodedKeySpec", ex); + } finally { + KeyUtil.clear(encoded); } } @@ -338,7 +340,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { /** * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified - * {@code DEREncodable}. A valid password-based encryption (PBE) algorithm + * {@code BinaryEncodable}. A valid password-based encryption (PBE) algorithm * and password must be specified. * *

The format of the PBE algorithm string is described in the @@ -346,7 +348,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { * Cipher Algorithms section of the Java Security Standard Algorithm Names * Specification. * - * @param de the {@code DEREncodable} to encrypt. Supported types include + * @param be the {@code BinaryEncodable} 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. @@ -354,68 +356,73 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { * @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. + * {@code Cipher} operations. If {@code null}, the default + * provider list is used. * @return an {@code EncryptedPrivateKeyInfo} - * @throws NullPointerException if {@code de}, {@code password}, or + * @throws NullPointerException if {@code be}, {@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 + * @throws IllegalArgumentException if {@code be} is an unsupported + * {@code BinaryEncodable} or has no encoding + * @throws CryptoException 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. + * encryption * - * @since 26 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, + public static EncryptedPrivateKeyInfo encrypt(BinaryEncodable be, char[] password, String algorithm, AlgorithmParameterSpec params, Provider provider) { - Objects.requireNonNull(de, "a key must be specified."); - Objects.requireNonNull(password, "a password must be specified."); - Objects.requireNonNull(algorithm, "an algorithm must be specified."); + Objects.requireNonNull(be, "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); + byte[] encoding = null; + SecretKey sk = null; try { - return encryptImpl(encoding, algorithm, - generateSecretKey(passwd, algorithm, provider), params, - provider, null); + encoding = getEncoding(be); + sk = generateSecretKey(passwd, algorithm, provider); + return encryptImpl(encoding, algorithm, sk, params, provider, null); } finally { + KeyUtil.destroySecretKeys(sk); KeyUtil.clear(passwd, encoding); } } /** * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified - * {@code DEREncodable}. A valid password must be specified. A default + * {@code BinaryEncodable}. A valid password must be specified. A default * password-based encryption (PBE) algorithm and provider are used. * - * @param de the {@code DEREncodable} to encrypt. Supported types include + * @param be the {@code BinaryEncodable} 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 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 + * @throws NullPointerException if {@code be} or {@code password} is {@code null} + * @throws IllegalArgumentException if {@code be} is an unsupported + * {@code BinaryEncodable} or has no encoding + * @throws CryptoException if an error occurs while generating the + * PBE key, if the default algorithm is misconfigured, or if an + * error occurs during encryption * * @implNote The {@code jdk.epkcs8.defaultAlgorithm} security property * defines the default encryption algorithm. The {@code AlgorithmParameterSpec} * defaults are determined by the provider. * - * @since 26 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, + public static EncryptedPrivateKeyInfo encrypt(BinaryEncodable be, char[] password) { - return encrypt(de, password, Pem.DEFAULT_ALGO, null, + return encrypt(be, password, Pem.DEFAULT_ALGO, null, null); } /** * Creates an {@code EncryptedPrivateKeyInfo} by encrypting the specified - * {@code DEREncodable}. A valid encryption algorithm and {@code Key} must + * {@code BinaryEncodable}. A valid encryption algorithm and {@code Key} must * be specified. * *

The format of the algorithm string is described in the @@ -423,36 +430,37 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { * Cipher Algorithms section of the Java Security Standard Algorithm Names * Specification. * - * @param de the {@code DEREncodable} to encrypt. Supported types include + * @param be the {@code BinaryEncodable} 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. + * @param random the {@code SecureRandom} instance used during encryption. + * If {@code null}, the default is used. * @return an {@code EncryptedPrivateKeyInfo} - * @throws NullPointerException if {@code de}, {@code encryptKey}, or + * @throws NullPointerException if {@code be}, {@code encryptKey}, or * {@code algorithm} is {@code null} - * @throws IllegalArgumentException if {@code de} is an unsupported - * {@code DEREncodable}, if {@code encryptKey} is invalid, if + * @throws IllegalArgumentException if {@code be} is an unsupported + * {@code BinaryEncodable} or has no encoding + * @throws CryptoException 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 26 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public static EncryptedPrivateKeyInfo encrypt(DEREncodable de, + public static EncryptedPrivateKeyInfo encrypt(BinaryEncodable be, 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, + Objects.requireNonNull(be, "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(be), algorithm, encryptKey, params, provider, random); } @@ -489,7 +497,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | IllegalStateException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) { - throw new IllegalArgumentException(e); + throw new CryptoException(e); } catch (BadPaddingException e) { throw new AssertionError(e); } finally { @@ -517,39 +525,39 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public PrivateKey getKey(char[] password) throws NoSuchAlgorithmException, InvalidKeyException { - Objects.requireNonNull(password, "a password must be specified."); + Objects.requireNonNull(password, "a password must be specified"); PBEKeySpec keySpec = new PBEKeySpec(password); + byte[] encoding = null; try { - return PKCS8Key.parseKey(Pem.decryptEncoding(this, keySpec), null); + encoding = Pem.decryptEncoding(this, keySpec); + return PKCS8Key.parseKey(encoding, null); } finally { keySpec.clearPassword(); + KeyUtil.clear(encoding); } } /** * Extracts and returns the enclosed {@code PrivateKey} using the specified - * decryption key and provider. + * decryption key. * - * @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. + * @param decryptKey the decryption key; must not be {@code null} * @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 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public PrivateKey getKey(Key decryptKey, Provider provider) + public PrivateKey getKey(Key decryptKey) throws NoSuchAlgorithmException, InvalidKeyException { - Objects.requireNonNull(decryptKey,"a decryptKey must be specified."); + Objects.requireNonNull(decryptKey,"a decryptKey must be specified"); byte[] encoding = null; try { - encoding = decryptData(decryptKey, provider); - return PKCS8Key.parseKey(encoding, provider); + encoding = decryptData(decryptKey, null); + return PKCS8Key.parseKey(encoding, null); } finally { KeyUtil.clear(encoding); } @@ -573,19 +581,22 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) public KeyPair getKeyPair(char[] password) throws NoSuchAlgorithmException, InvalidKeyException { - Objects.requireNonNull(password, "a password must be specified."); + Objects.requireNonNull(password, "a password must be specified"); PBEKeySpec keySpec = new PBEKeySpec(password); - DEREncodable d; + BinaryEncodable d; + byte[] encoding = null; try { - d = Pem.toDEREncodable(Pem.decryptEncoding(this, keySpec), true, null); + encoding = Pem.decryptEncoding(this, keySpec); + d = Pem.toPKCS8Encodable(encoding, null); } finally { keySpec.clearPassword(); + KeyUtil.clear(encoding); } return switch (d) { case KeyPair kp -> kp; case PrivateKey ignored -> throw new InvalidKeyException( - "This encoding does not contain a public key."); + "This encoding does not contain a public key"); default -> throw new InvalidKeyException( "Invalid class returned " + d.getClass().getName()); }; @@ -593,49 +604,52 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { /** * Extracts and returns the enclosed {@code KeyPair} using the specified - * decryption key and provider. If the encoded data does not contain both a + * decryption key. 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. + * @param decryptKey the decryption key; must not be {@code null} * @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 + * @since 27 */ @PreviewFeature(feature = PreviewFeature.Feature.PEM_API) - public KeyPair getKeyPair(Key decryptKey, Provider provider) + public KeyPair getKeyPair(Key decryptKey) throws NoSuchAlgorithmException, InvalidKeyException { - Objects.requireNonNull(decryptKey,"a decryptKey must be specified."); + Objects.requireNonNull(decryptKey,"a decryptKey must be specified"); - DEREncodable d = Pem.toDEREncodable( - decryptData(decryptKey, provider),true, provider); + BinaryEncodable d; + byte[] encoding = null; + try { + encoding = decryptData(decryptKey, null); + d = Pem.toPKCS8Encodable(encoding, null); + } finally { + KeyUtil.clear(encoding); + } return switch (d) { case KeyPair kp -> kp; case PrivateKey ignored -> throw new InvalidKeyException( - "This encoding does not contain a public key."); + "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. + * Extracts the enclosed PKCS8EncodedKeySpec object from the + * encrypted data and returns it. * @param decryptKey key used for decrypting the encrypted data. - * @return the PKCS8EncodedKeySpec object. + * @return the PKCS8EncodedKeySpec object with a specified algorithm * @exception NullPointerException if {@code decryptKey} * is {@code null}. * @exception NoSuchAlgorithmException if cannot find appropriate * cipher to decrypt the encrypted data. * @exception InvalidKeyException if {@code decryptKey} * cannot be used to decrypt the encrypted data or the decryption - * result is not a valid PKCS8KeySpec. + * result is not a valid PKCS8EncodedKeySpec. * * @since 1.5 */ @@ -648,12 +662,12 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } /** - * Extract the enclosed PKCS8EncodedKeySpec object from the - * encrypted data and return it. + * Extracts the enclosed PKCS8EncodedKeySpec object from the + * encrypted data and returns it. * @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 with a specified algorithm * @exception NullPointerException if {@code decryptKey} * or {@code providerName} is {@code null}. * @exception NoSuchProviderException if no provider @@ -662,7 +676,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { * cipher to decrypt the encrypted data. * @exception InvalidKeyException if {@code decryptKey} * cannot be used to decrypt the encrypted data or the decryption - * result is not a valid PKCS8KeySpec. + * result is not a valid PKCS8EncodedKeySpec. * * @since 1.5 */ @@ -670,7 +684,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { String providerName) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException { Objects.requireNonNull(decryptKey, "decryptKey is null"); - Objects.requireNonNull(providerName, "provider is null"); + Objects.requireNonNull(providerName, "providerName is null"); Provider provider = Security.getProvider(providerName); if (provider == null) { throw new NoSuchProviderException("provider " + @@ -680,19 +694,18 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } /** - * Extract the enclosed PKCS8EncodedKeySpec object from the - * encrypted data and return it. + * Extracts the enclosed PKCS8EncodedKeySpec object from the + * encrypted data and returns it. * @param decryptKey key used for decrypting the encrypted data. - * @param provider the name of provider whose cipher implementation - * will be used. - * @return the PKCS8EncodedKeySpec object. + * @param provider the provider whose cipher implementation will be used. + * @return the PKCS8EncodedKeySpec object with a specified algorithm * @exception NullPointerException if {@code decryptKey} * or {@code provider} is {@code null}. * @exception NoSuchAlgorithmException if cannot find appropriate * cipher to decrypt the encrypted data in {@code provider}. * @exception InvalidKeyException if {@code decryptKey} * cannot be used to decrypt the encrypted data or the decryption - * result is not a valid PKCS8KeySpec. + * result is not a valid PKCS8EncodedKeySpec. * * @since 1.5 */ @@ -745,22 +758,26 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { 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); + // Return the PKCS#8 encoding from a BinaryEncodable + private static byte[] getEncoding(BinaryEncodable d) { + try { + 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"); - }; + default -> throw new IllegalArgumentException( + d.getClass().getName() + " not supported by this method"); + }; + } catch (NullPointerException e) { + throw new IllegalArgumentException(e); + } } // Generate a SecretKey from the password. @@ -777,7 +794,7 @@ public non-sealed class EncryptedPrivateKeyInfo implements DEREncodable { } return factory.generateSecret(keySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalArgumentException(e); + throw new CryptoException(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 82c55d6f017..064e4e1fd92 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -68,8 +68,8 @@ public @interface PreviewFeature { STRUCTURED_CONCURRENCY, @JEP(number = 531, title = "Lazy Constants", status = "Third Preview") LAZY_CONSTANTS, - @JEP(number=524, title="PEM Encodings of Cryptographic Objects", - status="Second Preview") + @JEP(number=538, title="PEM Encodings of Cryptographic Objects", + status="Third Preview") PEM_API, LANGUAGE_MODEL, /** 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 154d1428414..9a4f3717065 100644 --- a/src/java.base/share/classes/sun/security/provider/X509Factory.java +++ b/src/java.base/share/classes/sun/security/provider/X509Factory.java @@ -581,7 +581,7 @@ public class X509Factory extends CertificateFactorySpi { } catch (EOFException e) { return null; } - return Base64.getDecoder().decode(rec.content()); + return Base64.getMimeDecoder().decode(rec.content()); } catch (IllegalArgumentException e) { throw new IOException(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 e9dabdc5b06..5a14deb70a4 100644 --- a/src/java.base/share/classes/sun/security/util/KeyUtil.java +++ b/src/java.base/share/classes/sun/security/util/KeyUtil.java @@ -31,6 +31,7 @@ import java.security.*; import java.security.interfaces.*; import java.security.spec.*; import java.util.Arrays; +import java.util.function.Function; import javax.crypto.SecretKey; import javax.crypto.interfaces.DHKey; import javax.crypto.interfaces.DHPublicKey; @@ -568,5 +569,25 @@ public final class KeyUtil { } } } + + /** + * Executes {@code op} with {@code encoding} and then zeroes {@code encoding} + * in a {@code finally} block before returning or propagating an exception. + * + * {@code encoding} is temporary sensitive data and is always wiped. + * + * Usage constraint: {@code op} must not return {@code encoding} itself, or + * any value backed by the same array. Otherwise, the returned data will already + * be zeroed when this method returns. + */ + public static T clear(byte[] encoding, Function op) { + try { + return op.apply(encoding); + } finally { + if (encoding != null) { + Arrays.fill(encoding, (byte)0); + } + } + } } 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 dac3eeec8b8..ae29951c4ef 100644 --- a/src/java.base/share/classes/sun/security/util/Pem.java +++ b/src/java.base/share/classes/sun/security/util/Pem.java @@ -29,6 +29,7 @@ import sun.security.pkcs.PKCS8Key; import sun.security.x509.AlgorithmId; import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.io.*; @@ -47,7 +48,11 @@ import java.util.regex.Pattern; * A utility class for PEM format encoding. */ public class Pem { - private static final byte[] CRLF = new byte[] {'\r', '\n'}; + private static final byte[] CRLF = new byte[]{'\r', '\n'}; + private static final byte[] DASH; + private static final byte[] BEGIN_B; + private static final byte[] BEGIN_PREFIX; + private static final byte[] END_PREFIX; // Default algorithm from jdk.epkcs8.defaultAlgorithm in java.security public static final String DEFAULT_ALGO; @@ -62,10 +67,10 @@ public class Pem { private static final Pattern LINE_WRAP_64_PATTERN; // Lazy initialized PBES2 OID value - private static ObjectIdentifier PBES2OID; + private static volatile ObjectIdentifier PBES2OID; // Lazy initialized singleton encoder. - private static Base64.Encoder b64Encoder; + private static volatile Base64.Encoder b64Encoder; static { String algo = Security.getProperty("jdk.epkcs8.defaultAlgorithm"); @@ -75,6 +80,10 @@ public class Pem { Pattern.CASE_INSENSITIVE); STRIP_WHITESPACE_PATTERN = Pattern.compile("\\s+"); LINE_WRAP_64_PATTERN = Pattern.compile("(.{64})"); + DASH = "-----".getBytes(StandardCharsets.ISO_8859_1); + BEGIN_B = "-----B".getBytes(StandardCharsets.ISO_8859_1); + BEGIN_PREFIX = "-----BEGIN ".getBytes(StandardCharsets.ISO_8859_1); + END_PREFIX = "-----END ".getBytes(StandardCharsets.ISO_8859_1); } public static final String CERTIFICATE = "CERTIFICATE"; @@ -162,13 +171,10 @@ public class Pem { * @param shortHeader if true, the hyphen length is 4 because the first * hyphen is assumed to have been read. This is needed * for the CertificateFactory X509 implementation. - * @return a new PEMRecord + * @return a PEM instance * @throws IOException on IO errors or PEM syntax errors that leave * the read position not at the end of a PEM block * @throws EOFException when at the unexpected end of the stream - * @throws IllegalArgumentException when a PEM syntax error occurs, - * but the read position in the stream is at the end of the block, so - * future reads can be successful. */ public static PEM readPEM(InputStream is, boolean shortHeader) throws IOException { @@ -176,257 +182,268 @@ public class Pem { int hyphen = (shortHeader ? 1 : 0); int eol = 0; - ByteArrayOutputStream os = new ByteArrayOutputStream(6); + var os = new ClearableBufferStream(6); // preData + var readbuf = new ByteArrayOutputStream(64); // header/footer + var pem = new ClearableBufferStream(1024); // PEM + String headerType, footerType; + byte[] encoding = null; - // Find 5 hyphens followed by a 'B' to start processing the header. - boolean headerStarted = false; - do { - int d = is.read(); - switch (d) { - case '-' -> hyphen++; - case -1 -> { - if (os.size() == 0) { - throw new EOFException("No data available"); + try { + // Find 5 hyphens followed by a 'B' to start processing the header. + boolean headerStarted = false; + do { + int d = is.read(); + switch (d) { + case '-' -> hyphen++; + case -1 -> { + if (os.size() == 0) { + throw new EOFException("No data available"); + } + throw new EOFException("No PEM data found"); } - throw new EOFException("No PEM data found"); - } - case 'B' -> { - if (hyphen == 5) { - headerStarted = true; - } else { - hyphen = 0; + case 'B' -> { + if (hyphen == 5) { + headerStarted = true; + } else { + hyphen = 0; + } } + default -> hyphen = 0; } - default -> hyphen = 0; - } - os.write(d); - } while (!headerStarted); + os.write(d); + } while (!headerStarted); - StringBuilder sb = new StringBuilder(64); - sb.append("-----B"); - hyphen = 0; - int c; + readbuf.writeBytes(BEGIN_B); + hyphen = 0; + int c; - // Get header definition until first hyphen - do { - switch (c = is.read()) { - case '-' -> hyphen++; - case -1 -> throw new EOFException("Input ended prematurely"); - case '\n', '\r' -> throw new IOException("Incomplete header"); - default -> sb.append((char) c); - } - } while (hyphen == 0); + // Get header definition until first hyphen + do { + switch (c = is.read()) { + case '-' -> hyphen++; + case -1 -> throw new EOFException("Input ended prematurely"); + case '\n', '\r' -> throw new IOException("Incomplete header"); + default -> readbuf.write(c); + } + } while (hyphen == 0); - // Verify header ending with 5 hyphens. - do { - switch (is.read()) { - case '-' -> hyphen++; - default -> + // Verify header ending with 5 hyphens. + do { + if (is.read() == '-') { + hyphen++; + } else { throw new IOException("Incomplete header"); + } + } while (hyphen < 5); + + readbuf.writeBytes(DASH); + byte[] header = readbuf.toByteArray(); + if (header.length < 16 || + !matchesAt(header, 0, BEGIN_PREFIX) || + !matchesAt(header, header.length - DASH.length, DASH)) { + throw new IOException("Illegal header: " + + new String(header, StandardCharsets.ISO_8859_1)); } - } while (hyphen < 5); - sb.append("-----"); - String header = sb.toString(); - if (header.length() < 16 || !header.startsWith("-----BEGIN ") || - !header.endsWith("-----")) { - throw new IOException("Illegal header: " + header); - } + hyphen = 0; + readbuf.reset(); - hyphen = 0; - sb = new StringBuilder(1024); + // Determine the line break using the char after the last hyphen + while (eol == 0) { + switch (is.read()) { + case '\s', '\t' -> {} // skip whitespace or tab + case '\r' -> { + c = is.read(); + if (c == '\n') { + eol = '\n'; + } else { + eol = '\r'; + pem.write(c); + } + } + case '\n' -> eol = '\n'; + default -> throw new IOException("No EOL character found"); + } + } - // Determine the line break using the char after the last hyphen - while (eol == 0) { - switch (is.read()) { - case '\s', '\t' -> {} // skip whitespace or tab - case '\r' -> { - c = is.read(); - if (c == '\n') { - eol = '\n'; - } else { - eol = '\r'; - sb.append((char) c); + // Read data until we find the first footer hyphen. + // CR & LF are allowed to support legacy PEM formats (ie: encrypted PKCS1) + do { + switch (c = is.read()) { + case -1 -> throw new EOFException("Incomplete header"); + case '-' -> hyphen++; + default -> { + // If reading a legacy format, allow for one dash + if (hyphen == 1) { + hyphen = 0; + pem.write('-'); + } + pem.write(c); } } - case '\n' -> eol = '\n'; - default -> throw new IOException("No EOL character found"); + } while (hyphen < 2); + + // Verify footer starts with 5 hyphens. + do { + switch (is.read()) { + case '-' -> hyphen++; + case -1 -> + throw new EOFException("Input ended prematurely"); + default -> throw new IOException("Incomplete footer"); + } + } while (hyphen < 5); + + hyphen = 0; + readbuf.reset(); + readbuf.writeBytes(DASH); + + // Look for Complete header by looking for the end of the hyphens + do { + switch (c = is.read()) { + case '-' -> hyphen++; + case -1 -> + throw new EOFException("Input ended prematurely"); + default -> readbuf.write(c); + } + } while (hyphen == 0); + + // Verify ending with 5 hyphens. + do { + switch (is.read()) { + case '-' -> hyphen++; + case -1 -> + throw new EOFException("Input ended prematurely"); + default -> throw new IOException("Incomplete footer"); + } + } while (hyphen < 5); + + while ((c = is.read()) != eol && c != -1) { + // skip when eol is '\n', the line separator is likely "\r\n". + if (c == '\r' || c == '\s' || c == '\t') { + continue; + } + throw new IOException("Invalid PEM format: " + + "No EOL char found in footer: 0x" + + HexFormat.of().toHexDigits((byte) c)); } + + readbuf.writeBytes(DASH); + byte[] footer = readbuf.toByteArray(); + if (footer.length < 14 || + !matchesAt(footer, 0, END_PREFIX) || + !matchesAt(footer, footer.length - DASH.length, DASH)) { + + // Not an IOE because the read pointer is correctly at the end. + throw new IOException("Illegal footer: " + + new String(footer, StandardCharsets.ISO_8859_1)); + } + + // Verify the object type in the header and the footer are the same. + headerType = new String(header, 11, header.length - 16, + StandardCharsets.ISO_8859_1); + footerType = new String(footer, 9, footer.length - 14, + StandardCharsets.ISO_8859_1); + if (!headerType.equals(footerType)) { + throw new IOException("Header and footer do not " + + "match: " + headerType + " " + footerType); + } + + // If there was data before finding the 5 dashes of the PEM header, + // backup 5 characters and save that data. + byte[] preData = null; + if (os.size() > 6) { + preData = Arrays.copyOf(os.getBuffer(), os.size() - 6); + } + + encoding = pem.toByteArray(); + return (preData == null) ? + new PEM(typeConverter(headerType), encoding) : + new PEM(typeConverter(headerType), encoding, preData); + } finally { + KeyUtil.clear(encoding); + os.close(); + pem.clear(); + pem.close(); + readbuf.close(); } - // Read data until we find the first footer hyphen. - do { - switch (c = is.read()) { - case -1 -> - throw new EOFException("Incomplete header"); - case '-' -> hyphen++; - case '\s', '\t', '\r', '\n' -> {} // skip whitespace and tab - default -> sb.append((char) c); - } - } while (hyphen == 0); - - String data = sb.toString(); - - // Verify footer starts with 5 hyphens. - do { - switch (is.read()) { - case '-' -> hyphen++; - case -1 -> throw new EOFException("Input ended prematurely"); - default -> throw new IOException("Incomplete footer"); - } - } while (hyphen < 5); - - hyphen = 0; - sb = new StringBuilder(64); - sb.append("-----"); - - // Look for Complete header by looking for the end of the hyphens - do { - switch (c = is.read()) { - case '-' -> hyphen++; - case -1 -> throw new EOFException("Input ended prematurely"); - default -> sb.append((char) c); - } - } while (hyphen == 0); - - // Verify ending with 5 hyphens. - do { - switch (is.read()) { - case '-' -> hyphen++; - case -1 -> throw new EOFException("Input ended prematurely"); - default -> throw new IOException("Incomplete footer"); - } - } while (hyphen < 5); - - while ((c = is.read()) != eol && c != -1 && c != '\s' && c != '\t') { - // skip when eol is '\n', the line separator is likely "\r\n". - if (c == '\r') { - continue; - } - throw new IOException("Invalid PEM format: " + - "No EOL char found in footer: 0x" + - HexFormat.of().toHexDigits((byte) c)); - } - - sb.append("-----"); - String footer = sb.toString(); - if (footer.length() < 14 || !footer.startsWith("-----END ") || - !footer.endsWith("-----")) { - // Not an IOE because the read pointer is correctly at the end. - throw new IOException("Illegal footer: " + footer); - } - - // Verify the object type in the header and the footer are the same. - String headerType = header.substring(11, header.length() - 5); - String footerType = footer.substring(9, footer.length() - 5); - if (!headerType.equals(footerType)) { - throw new IOException("Header and footer do not " + - "match: " + headerType + " " + footerType); - } - - // If there was data before finding the 5 dashes of the PEM header, - // backup 5 characters and save that data. - byte[] preData = null; - if (os.size() > 6) { - preData = Arrays.copyOf(os.toByteArray(), os.size() - 6); - } - - return new PEM(typeConverter(headerType), data, preData); } public static PEM readPEM(InputStream is) throws IOException { return readPEM(is, false); } - private static String pemEncoded(String type, String base64) { - return - "-----BEGIN " + type + "-----\r\n" + - base64 + (!base64.endsWith("\n") ? "\r\n" : "") + - "-----END " + type + "-----\r\n"; + /** + * Return a PEM encoding with the given type and base64 byte array. + */ + public static byte[] pemEncoded(String type, byte[] base64) { + byte[] header = ("-----BEGIN " + type + "-----\r\n") + .getBytes(StandardCharsets.ISO_8859_1); + byte[] footer = ("-----END " + type + "-----\r\n") + .getBytes(StandardCharsets.ISO_8859_1); + + int crlfLen = (base64.length == 0 || + base64[base64.length - 1] != '\n') ? 2 : 0; + byte[] result = new byte[header.length + base64.length + + crlfLen + footer.length]; + System.arraycopy(header, 0, result, 0, header.length); + System.arraycopy(base64, 0, result, header.length, base64.length); + if (crlfLen == 2) { + result[header.length + base64.length] = '\r'; + result[header.length + base64.length + 1] = '\n'; + } + System.arraycopy(footer, 0, result, + header.length + base64.length + crlfLen, footer.length); + return result; } - /** - * Construct a String-based encoding based off the type. leadingData - * is not used with this method. - * @return PEM in a string - */ - public static String pemEncoded(String type, byte[] der) { + public static byte[] pemEncodedFromDER(String type, byte[] der) { if (b64Encoder == null) { b64Encoder = Base64.getMimeEncoder(64, CRLF); } - return pemEncoded(type, b64Encoder.encodeToString(der)); + return KeyUtil.clear(b64Encoder.encode(der), e -> pemEncoded(type, e)); } /** - * Construct a String-based encoding based off the type. leadingData - * is not used with this method. - * @return PEM in a string + * Decrypt the EncryptedPrivateKeyInfo with the given keySpec and + * return the PKCS#8 byte array */ - public static String pemEncoded(PEM pem) { - String p = LINE_WRAP_64_PATTERN.matcher(pem.content()).replaceAll("$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 { + public static byte[] decryptEncoding(EncryptedPrivateKeyInfo ekpi, + PBEKeySpec keySpec) throws NoSuchAlgorithmException, + InvalidKeyException { PKCS8EncodedKeySpec p8KeySpec = null; + SecretKeyFactory skf = SecretKeyFactory.getInstance(ekpi.getAlgName()); + SecretKey sk = null; try { - SecretKeyFactory skf = SecretKeyFactory.getInstance(ekpi.getAlgName()); - p8KeySpec = ekpi.getKeySpec(skf.generateSecret(keySpec)); + sk = skf.generateSecret(keySpec); + p8KeySpec = ekpi.getKeySpec(sk); return p8KeySpec.getEncoded(); } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } finally { + KeyUtil.destroySecretKeys(sk); 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, + public static BinaryEncodable toPKCS8Encodable(byte[] encoded, Provider provider) throws InvalidKeyException { PrivateKey privKey; PublicKey pubKey = null; + KeyFactory kf; 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); - } + p8KeySpec = new PKCS8EncodedKeySpec(encoded); try { if (provider == null) { @@ -442,12 +459,6 @@ public class Pem { 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( @@ -467,10 +478,43 @@ public class Pem { } finally { KeyUtil.clear(p8KeySpec, p8key); } - if (pair && pubKey != null) { + if (pubKey != null) { return new KeyPair(pubKey, privKey); } return privKey; } + private static boolean matchesAt(byte[] source, int offset, byte[] match) { + for (int i = 0; i < match.length; i++) { + if (source[offset + i] != match[i]) { + return false; + } + } + return true; + } + + /** + * Clearable ByteArrayOutputStream for temporary data. Access to the + * internal buffer is allowed to limit data copying. Handle with care. + */ + private static final class ClearableBufferStream + extends ByteArrayOutputStream { + + ClearableBufferStream(int len) { + super(len); + } + + byte[] getBuffer() { + return buf; + } + + int length() { + return count; + } + + void clear() { + Arrays.fill(buf, (byte) 0); + count = 0; + } + } } diff --git a/test/jdk/java/security/PEM/PEMData.java b/test/jdk/java/security/PEM/PEMData.java index 0005ec4e582..09c4663aeee 100644 --- a/test/jdk/java/security/PEM/PEMData.java +++ b/test/jdk/java/security/PEM/PEMData.java @@ -24,7 +24,8 @@ */ import javax.crypto.EncryptedPrivateKeyInfo; -import java.security.DEREncodable; +import java.nio.charset.StandardCharsets; +import java.security.BinaryEncodable; import java.security.KeyPair; import java.security.PEM; import java.security.cert.X509CRL; @@ -32,7 +33,9 @@ import java.security.cert.X509Certificate; import java.security.interfaces.*; import java.util.ArrayList; import java.util.Base64; +import java.util.HexFormat; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -399,26 +402,26 @@ class PEMData { 6onPAs4hkm+63dfzCojvEkALevO8J3OVX7YS5q9J1r75wDn60Ob0Zh+iiorpx8Ob WqcWcoJqfdLEyBT+ -----END PRIVATE KEY----- - """, DEREncodable.class, null); + """, BinaryEncodable.class, null); private static final Entry invalidPEM = new Entry("invalidPEM", """ -----BEGIN INVALID PEM----- MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBVS52ZSKZ0oES7twD2 GGwRIVu3uHlGIwlu0xzFe7sgIPntca2bHfYMhgGxrlCm0q+hZANiAAQNWgwWfLX8 8pYVjvwbfvDF9f+Oa9w6JjrfpWwFAUI6b1OPgrNUh+yXtUXnQNXnfUcIu0Os53bM - """, DEREncodable.class, null); + """, BinaryEncodable.class, null); private static final Entry invalidHeader = new Entry("invalidHeader", """ ---BEGIN PRIVATE KEY--- MC4CAQAwBQYDK2VwBCIEIFFZsmD+OKk67Cigc84/2fWtlKsvXWLSoMJ0MHh4jI4I -----END PRIVATE KEY----- - """, DEREncodable.class, null); + """, BinaryEncodable.class, null); private static final Entry invalidFooter = new Entry("invalidFooter", """ -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIFFZsmD+OKk67Cigc84/2fWtlKsvXWLSoMJ0MHh4jI4I ---END PRIVATE KEY--- - """, DEREncodable.class, null); + """, BinaryEncodable.class, null); private static final Entry incorrectFooter = new Entry("incorrectFooter", """ -----BEGIN PRIVATE KEY----- @@ -427,7 +430,7 @@ class PEMData { 8pYVjvwbfvDF9f+Oa9w6JjrfpWwFAUI6b1OPgrNUh+yXtUXnQNXnfUcIu0Os53bM 8fTqPkQl6RyWEDHeXqJK8zTBHMeBq9nLfDPSbzQgLDyC64Orn0D8exM= -----END PUBLIC KEY----- - """, DEREncodable.class, null); + """, BinaryEncodable.class, null); // EC cert with explicit parameters -- Not currently supported by SunEC static final String ecCertEX = """ @@ -498,6 +501,41 @@ class PEMData { -----END CERTIFICATE REQUEST----- """ + postData, PEM.class, "SunEC"); + // This PEM is not valid, it is only used to verify PEMDecoder will + // parse the non-base64 text properly. + public static final String encpkcs1 = """ + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,ECBE98FA344F0C87 + + MIIEowIBAAKCAQEAqozTLan1qFcOCWnS63jXQn5lLyGOKDv3GM11n2zkGGrChayj + cSzB2KTlDmN9NgOyFdqGNWbSgdmXR5ToHGHYwaKubJoQIoPQcsipWDI156d3+X/8 + BxCGY8l5nYwvS4olOXc+2kEjeFF1eamnm9IQ5DHZfaFPl0ri4Yfm1YHBAbt/7HvF + 3MBjgBj1xSsSFLW4O6ws6guRVGDfKBVyyRNUhRTbSua/nEz0wAjxF2PWT+ZTHS6M + 0siYwVTuPI4/n4ItoYoahvGb9JskkXP+bc/QZJCTFYdyxF5tKqVMSdYaJTxop02p + Jo3oeafVKSlBrr0K731xgNBKqBud44aKT5R96QIDAQABAoIBAQCD9Q/T7gOvayPm + LqXOISJURV1emRTXloX5/8Y5QtQ8/CVjrg6Lm3ikefjsKBgR+cwJUpmyqcrIQyXk + cZchlqdSMt/IEW/YdKqMlStJnRfOE+ok9lx2ztdcT9+0AWn6hXmFu/i6f9nE1yoQ + py6SxnbhSJyhsnTVd1CR9Uep/InsHvYW/15WlVMD1VuCSIt9sefqXwavbAfBaqbn + mjwBB/ulsqKhHSuRq/QWqlj+jyGqhhYmTguC1Qwt0woDbThiHtK+suCTAlGBj/A+ + IZ1U9d+VsHBcWDKBkxmlKWcJAGR3xXiKKy9vfzC+DU7L99kgay80VZarDyXgiy78 + 9xMMzRMBAoGBANoxnZhu1bUFtLqTJ1HfDm6UB+1zVd2Mu4DXYdy/AHjoaCLp05OQ + 0ZeyhO/eXPT+eGpzCxkWD7465KO/QDfnp54p/NS73jaJVdWQHBhzJx1MymqURy3N + JQeW4+ojzwSmVXcrs7Og6EBa4L+PWLpMLW2kODniCY+vp9f5LS6m8UPJAoGBAMgZ + 4rBw7B9YFZZW/EE4eos4Q7KtA5tEP6wvCq04oxfiSytWXifYX0ToPp0CHhZlWOxk + v9a/BDGqM7AxAQJs7mmIvT5AT2V1w7oTbFPnnAo6pQtLcfaxdFFqr0h6t0sXSOKC + rQeZAqqFqwuOyP7vT0goGlBruHkwS21NKkzCyzkhAoGAc2JjhbWu+8Cdt0CUPX5o + ol9T5eTlFnkSuuqrTNIQzN+SGkxu341o2QDFvhdoLwLW6OwXhVeeUanROSqtKiMu + B70Kf/EtbMephXtk8CUNHTh7nmr1TSo8F8xakHoJQts3PQL2T9qal1W3nnWOpU4d + g+qg9TMsfTiV2OdjVlVgJskCgYBSnjV1qjojuue22hVvDFW0c7en5z2M9wHfItEi + sjbMnrdwnklj5Dd5qPZpNz2a+59ag0Kd9OJTazXKMoF7MeTCGB4ivMTLXHNCudBJ + WGCZ7JrGbhEQzTX8g7L5lwlk7KlANLoiX++03lm//OVKNR6j6ULsH33cM6+A4pJr + fSYRYQKBgCr9iMTmL0x+n6AmMNecR+MhDxi99Oy0s2EBAYqN9g/8yNgwM4KR0cjz + EcgIOtkvoTrJ9Cquvuj+O7/d2yNoH0SZQ4IYJKq47/Z4kKhwXzJnBCCCBKgkjfub + RTQSNnSEgTaBD29l7FrhNRHX9lIKFZ23caCTBS6o3q3+KgPbq7ao + -----END RSA PRIVATE KEY----- + """; + final static Pattern CR = Pattern.compile("\r"); final static Pattern LF = Pattern.compile("\n"); final static Pattern LSDEFAULT = Pattern.compile(System.lineSeparator()); @@ -543,8 +581,8 @@ class PEMData { clazz, provider, password()); } - Entry makeCR(String name) { - return new Entry(name, + Entry makeCR(String newName) { + return new Entry(newName, Pattern.compile(System.lineSeparator()).matcher(pem).replaceAll("\r"), clazz, provider, password()); } @@ -555,6 +593,32 @@ class PEMData { replaceAll(""), clazz, provider, password()); } + + Entry makeValidNoCRLF(String newName) { + int cr = pem.indexOf('\r'); + int lf = pem.indexOf('\n'); + if (cr == -1 && lf == -1) { + throw new AssertionError("Cannot find EOL with " + newName); + } + + Matcher eol; + int cut; // index right after the first EOL + + if (lf - cr == 1) { + eol = LSDEFAULT.matcher(pem); + cut = lf + 1; + } else if (cr != -1 && cr < lf) { + eol = CR.matcher(pem); + cut = cr + 1; + } else { + eol = LF.matcher(pem); + cut = lf + 1; + } + + String p = pem.substring(0, cut) + + eol.pattern().matcher(pem.substring(cut)).replaceAll(""); + return new Entry(newName, p, clazz, provider, password()); + } } static public Entry getEntry(String varname) { @@ -613,8 +677,8 @@ class PEMData { passList.addAll(entryList); passList.addAll(encryptedList); - failureEntryList.add(new Entry("emptyPEM", "", DEREncodable.class, null)); - failureEntryList.add(new Entry("nullPEM", null, DEREncodable.class, null)); + failureEntryList.add(new Entry("emptyPEM", "", BinaryEncodable.class, null)); + failureEntryList.add(new Entry("nullPEM", null, BinaryEncodable.class, null)); failureEntryList.add(incorrectFooter); failureEntryList.add(invalidPEM); failureEntryList.add(invalidDer); @@ -649,6 +713,18 @@ class PEMData { } } + static void checkResultsExact(String expected, String result) { + try { + if (expected.compareTo(result) != 0) { + System.out.println("expected:\n" + expected); + System.out.println("generated:\n" + result); + indexDiff(expected, result); + } + } catch (AssertionError e) { + throw new AssertionError("Encoder PEM mismatch "); + } + } + static void indexDiff(String a, String b) { String lenerr = ""; int len = a.length(); diff --git a/test/jdk/java/security/PEM/PEMDecoderTest.java b/test/jdk/java/security/PEM/PEMDecoderTest.java index b04c31f1ebd..dc0fe18956d 100644 --- a/test/jdk/java/security/PEM/PEMDecoderTest.java +++ b/test/jdk/java/security/PEM/PEMDecoderTest.java @@ -65,7 +65,7 @@ public class PEMDecoderTest { System.out.println("Decoder test withFactory:"); PEMData.entryList.forEach(entry -> test(entry, true)); System.out.println("Decoder test returning DEREncodable class:"); - PEMData.entryList.forEach(entry -> test(entry, DEREncodable.class)); + PEMData.entryList.forEach(entry -> test(entry, BinaryEncodable.class)); System.out.println("Decoder test with encrypted PEM:"); PEMData.encryptedList.forEach(PEMDecoderTest::testEncrypted); System.out.println("Decoder test with OAS:"); @@ -101,7 +101,7 @@ public class PEMDecoderTest { System.out.println("Decoder test ecsecp256 to P8EKS:"); decr.decode(PEMData.ecsecp256.pem(), PKCS8EncodedKeySpec.class); - System.out.println("Checking if decode() returns the same encoding:"); + System.out.println("Checking if content() returns the same encoding:"); PEMData.privList.forEach(PEMDecoderTest::testDERCheck); PEMData.oasList.forEach(PEMDecoderTest::testDERCheck); @@ -110,21 +110,22 @@ public class PEMDecoderTest { PEMData.oasList.stream().filter(e -> !e.name().endsWith("xdh")) .forEach(PEMDecoderTest::testSignature); - System.out.println("Checking if decode() returns a PKCS8Key and can generate a pub"); + System.out.println("Checking if content() returns a PKCS8Key and can generate a pub"); PEMData.oasList.forEach(PEMDecoderTest::testPKCS8Key); System.out.println("Checking if ecCSR:"); test(PEMData.ecCSR); System.out.println("Checking if ecCSR with preData:"); - DEREncodable result = d.decode(PEMData.ecCSRWithData.pem(), PEM.class); + BinaryEncodable 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()) + "\""); throw new AssertionError("ecCSRWithData preData wrong"); } - if (rec.content().lastIndexOf("F") > rec.content().length() - 5) { - System.err.println("received: " + rec.content()); + String s = rec.toString(); + if (s.lastIndexOf("F") > s.length() - 5) { + System.err.println("received: " + s); throw new AssertionError("ecCSRWithData: " + "End of PEM data has an unexpected character"); } @@ -165,16 +166,6 @@ public class PEMDecoderTest { } System.out.println("PASS"); - System.out.println("Check if PEM decode decodes content correctly"); - final String tmpContent = "1234567"; - final PEM tmpPem = new PEM("ONE", tmpContent); - if (!Arrays.equals( - Base64.getDecoder().decode(tmpContent), - tmpPem.decode())) { - throw new AssertionError("PEM decode error"); - } - System.out.println("PASS"); - System.out.println("Decode to EncryptedPrivateKeyInfo: "); EncryptedPrivateKeyInfo ekpi = d.decode(PEMData.ed25519ep8.pem(), EncryptedPrivateKeyInfo.class); @@ -217,7 +208,13 @@ public class PEMDecoderTest { 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); + d.decode(bis, PEM.class); + + d.decode(PEMData.encpkcs1, PEM.class); + try { + d.decode(PEMData.ecCSR.pem(), BinaryEncodable.class); + throw new AssertionError("decode(s, BinaryEncodable.class passed"); + } catch (ClassCastException _) {} } static void testInputStream() throws IOException { @@ -287,12 +284,14 @@ public class PEMDecoderTest { static void testPEMRecord(PEMData.Entry entry) { PEM r = d.decode(entry.pem(), PEM.class); - String expected = entry.pem().split("-----")[2].replace(System.lineSeparator(), ""); + int start = entry.pem().indexOf("-----"); + int end = entry.pem().lastIndexOf("-----"); + String expected = entry.pem().substring(start, end); try { - PEMData.checkResults(expected, r.content()); + PEMData.checkResults(expected, r.toString()); } catch (AssertionError e) { System.err.println("expected:\n" + expected); - System.err.println("received:\n" + r.content()); + System.err.println("received:\n" + r); throw e; } @@ -322,7 +321,7 @@ public class PEMDecoderTest { static void testPEMRecordDecode(PEMData.Entry entry) { PEM r = d.decode(entry.pem(), PEM.class); - DEREncodable de = d.decode(r.toString()); + BinaryEncodable de = d.decode(r.toString()); boolean result = switch(r.type()) { case Pem.PRIVATE_KEY -> @@ -371,7 +370,7 @@ public class PEMDecoderTest { } } - static DEREncodable testEncrypted(PEMData.Entry entry) { + static BinaryEncodable testEncrypted(PEMData.Entry entry) { PEMDecoder decoder; if (!Objects.equals(entry.clazz(), EncryptedPrivateKeyInfo.class)) { decoder = d.withDecryption(entry.password()); @@ -388,27 +387,27 @@ public class PEMDecoderTest { } // Change the Entry to use the given class as the expected class returned - static DEREncodable test(PEMData.Entry entry, Class c) { + static BinaryEncodable test(PEMData.Entry entry, Class c) { return test(entry.newClass(c), false); } // Run test with a given Entry - static DEREncodable test(PEMData.Entry entry) { + static BinaryEncodable test(PEMData.Entry entry) { return test(entry, false); } // Run test with a given Entry - static DEREncodable test(PEMData.Entry entry, boolean withFactory) { + static BinaryEncodable test(PEMData.Entry entry, boolean withFactory) { System.out.printf("Testing %s %s%n", entry.name(), entry.provider()); try { PEMDecoder pemDecoder; if (withFactory) { Provider provider = Security.getProvider(entry.provider()); - pemDecoder = d.withFactory(provider); + pemDecoder = d.withFactoriesOf(provider); } else { pemDecoder = d; } - DEREncodable r = test(entry.pem(), entry.clazz(), pemDecoder); + BinaryEncodable r = test(entry.pem(), entry.clazz(), pemDecoder); System.out.println("PASS (" + entry.name() + ")"); return r; } catch (Exception | AssertionError e) { @@ -444,9 +443,9 @@ public class PEMDecoderTest { * Perform the decoding test with the given decoder, on the given pem, and * expect the clazz to be returned. */ - static DEREncodable test(String pem, Class clazz, PEMDecoder decoder) + static BinaryEncodable test(String pem, Class clazz, PEMDecoder decoder) throws IOException { - DEREncodable pk = decoder.decode(pem); + BinaryEncodable pk = decoder.decode(pem); // Check that clazz matches what pk returned. if (pk.getClass().equals(clazz)) { @@ -553,7 +552,7 @@ public class PEMDecoderTest { byte[] data = "12345678".getBytes(); PrivateKey privateKey; - DEREncodable der = d.decode(entry.pem()); + BinaryEncodable der = d.decode(entry.pem()); switch (der) { case PrivateKey p -> privateKey = p; case KeyPair kp -> privateKey = kp.getPrivate(); diff --git a/test/jdk/java/security/PEM/PEMEncoderTest.java b/test/jdk/java/security/PEM/PEMEncoderTest.java index 4b60758c89f..4d205f2f9bf 100644 --- a/test/jdk/java/security/PEM/PEMEncoderTest.java +++ b/test/jdk/java/security/PEM/PEMEncoderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -56,7 +56,7 @@ import static jdk.test.lib.Asserts.assertThrows; public class PEMEncoderTest { - static Map keymap; + static Map keymap; static String pkcs8DefaultAlgExpect; public static void main(String[] args) throws Exception { @@ -79,8 +79,12 @@ public class PEMEncoderTest { keymap.keySet().forEach(key -> test(key, PEMEncoder.of())); System.out.println("Same instance re-encode testToString:"); keymap.keySet().forEach(key -> testToString(key, encoder)); + System.out.println("Same instance encode/encodeToString consistency test:"); + keymap.keySet().forEach(key -> testEncodeConsistency(key, encoder)); System.out.println("New instance re-encode testToString:"); keymap.keySet().forEach(key -> testToString(key, PEMEncoder.of())); + System.out.println("New instance encode/encodeToString consistency test:"); + keymap.keySet().forEach(key -> testEncodeConsistency(key, PEMEncoder.of())); System.out.println("Same instance Encoder testEncodedKeySpec:"); testEncodedKeySpec(encoder); System.out.println("New instance Encoder testEncodedKeySpec:"); @@ -90,6 +94,8 @@ public class PEMEncoderTest { keymap = generateObjKeyMap(PEMData.encryptedList); System.out.println("Same instance Encoder match test:"); keymap.keySet().forEach(key -> testEncryptedMatch(key, encoder)); + System.out.println("Same instance encrypted encode/encodeToString consistency test:"); + keymap.keySet().forEach(key -> testEncodeConsistency(key, encoder)); System.out.println("Same instance Encoder new withEnc test:"); keymap.keySet().forEach(key -> testEncrypted(key, encoder)); System.out.println("New instance Encoder and withEnc test:"); @@ -150,10 +156,36 @@ public class PEMEncoderTest { throw new AssertionError("encoder tried to encrypt " + "an EncryptedPrivateKeyInfo."); } catch (IllegalArgumentException _) {} + + // Check PEM string exact + String expected = encoder.encodeToString(decoder.decode( + PEMData.ecsecp256.pem())); + PEMData.Entry e = PEMData.ecsecp256.makeCRLF("ecsecp256CRLF"); + System.out.println("Exact PEM String check with CRLF only PEM:"); + PEMData.checkResultsExact(expected, encoder.encodeToString( + decoder.decode(e.pem()))); + System.out.println("Exact PEM String check with CR only PEM:"); + e = PEMData.ecsecp256.makeCR("ecsecp256CR"); + PEMData.checkResultsExact(expected, encoder.encodeToString( + decoder.decode(e.pem()))); + System.out.println("Exact PEM String check with NoCRLF only PEM:"); + e = PEMData.ecsecp256.makeValidNoCRLF("ecsecp256ValidNoCRLF"); + System.out.println(HexFormat.of().formatHex(e.pem().getBytes(StandardCharsets.UTF_8))); + System.out.println("EOL: " + HexFormat.of().formatHex(System.lineSeparator().getBytes(StandardCharsets.UTF_8))); + PEMData.checkResultsExact(expected, encoder.encodeToString( + decoder.decode(e.pem()))); + + // Independent structural check for the new byte-oriented utility path. + System.out.println("Testing consistency between pemEncodedFromArray()" + + "and pemEncoded():"); + testPemEncodedFromArray(); + + // Encode an empty PEM content + encoder.encode(new PEM("X", "")); } static Map generateObjKeyMap(List list) { - Map keymap = new HashMap<>(); + Map keymap = new HashMap<>(); PEMDecoder pemd = PEMDecoder.of(); for (PEMData.Entry entry : list) { try { @@ -202,6 +234,49 @@ public class PEMEncoderTest { System.out.println("PASS: " + entry.name()); } + static void testEncodeConsistency(String key, PEMEncoder encoder) { + byte[] encoding; + String pem; + PEMData.Entry entry = PEMData.getEntry(key); + try { + encoding = encoder.encode(keymap.get(key)); + pem = encoder.encodeToString(keymap.get(key)); + } catch (RuntimeException e) { + throw new AssertionError("Encoder consistency failure with " + + entry.name(), e); + } + + assertEquals(new String(encoding, StandardCharsets.ISO_8859_1), pem); + System.out.println("PASS: " + entry.name()); + } + + static void testPemEncodedFromArray() { + byte[] data = {1, 2, 3, 4, 5}; + String type = Pem.CERTIFICATE; + String base64 = Base64.getMimeEncoder(64, "\r\n".getBytes( + StandardCharsets.ISO_8859_1)).encodeToString(data); + var expected = ("-----BEGIN " + type + "-----\r\n" + + base64 + (!base64.endsWith("\n") ? "\r\n" : "") + + "-----END " + type + "-----\r\n"); + var result = Pem.pemEncoded(type, base64.getBytes(StandardCharsets.ISO_8859_1)); + if (!Arrays.equals(result, expected.getBytes(StandardCharsets.ISO_8859_1))) { + throw new AssertionError( + "result =\n" + new String(result, StandardCharsets.ISO_8859_1) + + "expected =\n " + expected); + } + + // Empty data should still include a CRLF before footer. + byte[] empty = new byte[0]; + String emptyBase64 = Base64.getMimeEncoder(64, "\r\n".getBytes( + StandardCharsets.ISO_8859_1)).encodeToString(empty); + String emptyExpected = "-----BEGIN " + type + "-----\r\n" + + emptyBase64 + (!emptyBase64.endsWith("\n") ? "\r\n" : "") + + "-----END " + type + "-----\r\n"; + assertEquals(new String(Pem.pemEncoded(type, empty), + StandardCharsets.ISO_8859_1), emptyExpected); + System.out.println("PASS"); + } + /* Test cannot verify PEM was the same as known PEM because we have no public access to the AlgoritmID.params and PBES2Parameters. @@ -237,7 +312,7 @@ public class PEMEncoderTest { try { encoder.encodeToString(keymap.get(key)); } catch (RuntimeException e) { - throw new AssertionError("Encrypted encoder failured with " + + throw new AssertionError("Encrypted encoder failed with " + entry.name(), e); } diff --git a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/DisableRevocation.java b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/DisableRevocation.java index 23a0b11d93c..1f48b077aca 100644 --- a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/DisableRevocation.java +++ b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/DisableRevocation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,13 +41,11 @@ */ import java.io.*; -import java.net.SocketException; -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; import java.security.cert.*; -import java.security.cert.CertPathValidatorException.BasicReason; import sun.security.util.DerInputStream; /** @@ -161,7 +159,7 @@ public final class DisableRevocation { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); // generate certificate from certificate string entries.add(PEM_DECODER.decode(targetCertStr, X509Certificate.class)); diff --git a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/KeyUsageMatters.java b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/KeyUsageMatters.java index 1343aefa7f3..fc49aa69f84 100644 --- a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/KeyUsageMatters.java +++ b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/KeyUsageMatters.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -40,14 +40,12 @@ * @author Xuelei Fan */ -import java.io.*; -import java.net.SocketException; -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; import java.security.cert.*; -import java.security.cert.CertPathValidatorException.BasicReason; + import sun.security.util.DerInputStream; /** @@ -196,7 +194,7 @@ public final class KeyUsageMatters { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); // Decode and add certificates entries.add(PEM_DECODER.decode(targetCertStr, X509Certificate.class)); diff --git a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/StatusLoopDependency.java b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/StatusLoopDependency.java index 2a1514fae8a..95b02811a20 100644 --- a/test/jdk/java/security/cert/CertPathBuilder/selfIssued/StatusLoopDependency.java +++ b/test/jdk/java/security/cert/CertPathBuilder/selfIssued/StatusLoopDependency.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -39,7 +39,7 @@ * @run main/othervm StatusLoopDependency alice */ -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.security.Security; import java.security.cert.CertPathBuilder; @@ -210,9 +210,9 @@ public final class StatusLoopDependency { private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); - DEREncodable cert = pemDecoder.decode(targetCertStr, X509Certificate.class); + BinaryEncodable cert = pemDecoder.decode(targetCertStr, X509Certificate.class); entries.add(cert); cert = pemDecoder.decode(subCaCertStr, X509Certificate.class); @@ -228,7 +228,7 @@ public final class StatusLoopDependency { entries.add(cert); // generate CRL from CRL string - DEREncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); + BinaryEncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); entries.add(mixes); mixes = pemDecoder.decode(subCrlStr, X509CRL.class); diff --git a/test/jdk/java/security/cert/CertPathValidator/OCSP/FailoverToCRL.java b/test/jdk/java/security/cert/CertPathValidator/OCSP/FailoverToCRL.java index 2232ae06711..3bf801b8255 100644 --- a/test/jdk/java/security/cert/CertPathValidator/OCSP/FailoverToCRL.java +++ b/test/jdk/java/security/cert/CertPathValidator/OCSP/FailoverToCRL.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -136,13 +136,11 @@ */ import java.io.*; -import java.net.SocketException; -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; import java.security.cert.*; -import java.security.InvalidAlgorithmParameterException; import java.security.cert.CertPathValidatorException.BasicReason; import java.util.Collections; @@ -232,7 +230,7 @@ public class FailoverToCRL { new ByteArrayInputStream(crlStr.getBytes()); // generate a cert store - Collection crls = new HashSet<>(); + Collection crls = new HashSet<>(); crls.add(PEM_DECODER.decode(crlStr, X509CRL.class)); diff --git a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevel.java b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevel.java index 947f0e0b485..81b6a4d1351 100644 --- a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevel.java +++ b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,9 +37,7 @@ * @author Xuelei Fan */ -import java.io.*; -import java.net.SocketException; -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; @@ -142,7 +140,7 @@ public class CircularCRLOneLevel { private static CertStore generateCertificateStore() throws Exception { // generate a cert store - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); entries.add(PEM_DECODER.decode(crlStr, X509CRL.class)); entries.add(PEM_DECODER.decode(crlIssuerCertStr, X509Certificate.class)); diff --git a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevelRevoked.java b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevelRevoked.java index 251883d4e51..caedb27f823 100644 --- a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevelRevoked.java +++ b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLOneLevelRevoked.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,9 +37,7 @@ * @author Xuelei Fan */ -import java.io.*; -import java.net.SocketException; -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; @@ -142,7 +140,7 @@ public class CircularCRLOneLevelRevoked { private static CertStore generateCertificateStore() throws Exception { // generate CRL from CRL string - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); entries.add(PEM_DECODER.decode(crlStr, X509CRL.class)); entries.add(PEM_DECODER.decode(crlIssuerCertStr, X509Certificate.class)); diff --git a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevel.java b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevel.java index c83f1bc1f5d..4a5d9fe409a 100644 --- a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevel.java +++ b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,7 +37,7 @@ * @author Xuelei Fan */ -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.security.Security; import java.security.cert.CertPath; @@ -198,11 +198,11 @@ public class CircularCRLTwoLevel { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); // generate CRL from CRL string - DEREncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); + BinaryEncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); entries.add(mixes); mixes = pemDecoder.decode(subCrlStr, X509CRL.class); diff --git a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevelRevoked.java b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevelRevoked.java index 7ac63072737..909b3e53a14 100644 --- a/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevelRevoked.java +++ b/test/jdk/java/security/cert/CertPathValidator/indirectCRL/CircularCRLTwoLevelRevoked.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -37,7 +37,7 @@ * @author Xuelei Fan */ -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.security.Security; import java.security.cert.CertPath; @@ -198,12 +198,12 @@ public class CircularCRLTwoLevelRevoked { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); // generate CRL from CRL string CertificateFactory cf = CertificateFactory.getInstance("X.509"); - DEREncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); + BinaryEncodable mixes = pemDecoder.decode(topCrlStr, X509CRL.class); entries.add(mixes); mixes = pemDecoder.decode(subCrlStr, X509CRL.class); diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java index abeed7c3395..631410efb21 100644 --- a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/Encrypt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -129,7 +129,7 @@ public class Encrypt { // 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); + PrivateKey key3 = e.getKey(key2); 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 36e6b02faba..a4bfd4e47d6 100644 --- a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,8 +35,6 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.PEMDecoder; import java.security.PrivateKey; -import java.security.Provider; -import java.security.Security; import java.util.Arrays; public class GetKey { @@ -50,30 +48,13 @@ public class GetKey { IycFtI70ciPjgwDSjtCcPxR8fSxJPrm2yOJsRVo= -----END ENCRYPTED PRIVATE KEY----- """; - private static final String encDHECKey = - """ - -----BEGIN ENCRYPTED PRIVATE KEY----- - MIIBvDBmBgkqhkiG9w0BBQ0wWTA4BgkqhkiG9w0BBQwwKwQUN8pkErJx7aqH0fJF - BcOadPKiuRoCAhAAAgEQMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBAT1Vwd - gU4rTd6zy7lKr4wmBIIBUMe+2+O0AG6t4CMSHcDVceRg2jvbs5PmPjW4Ka5mDich - hVEsjSpJLbUyJdbji6UaiUpuWgvYSMLZ10pfhOFw/ssXwCw+JrlXUqDpQGLaW8ZR - zSL3CoozTI2Y6EBdWt53KbySwtZMoTpW/W3vPi98bJXtR635msf6gYXmSUP7DyoJ - 79dxz3pRYsnOuBe0yZ2wTq9iMgTMudzLJAFX2qyi+3KOb1g5Va9DYAqJmzCYOd74 - +I+0gGNFtSc1vGQYr3cAfcKT8AZ1RHE4IkpnpgFD5HsZ8f4hy0yK8juk9NE9Gzuy - B929LBXk6V3L0MKzIABS3QvAlhWETM6XtGBDugzAgsooo9lEHLwYRldvOlL+QYyE - CtqDmXOrgEMWvxWGEFCTKYhKkqMKjU3y3GiozEEdb9j2okW1s30yHQjIoj0OR4nB - D8GeP0QnY73NfbOw7z81TA== - -----END ENCRYPTED PRIVATE KEY----- - """; + private static final String passwdText = "fish"; private static final char[] password = passwdText.toCharArray(); private static final SecretKey key = new SecretKeySpec( passwdText.getBytes(), "PBE"); public static void main(String[] args) throws Exception { - Provider p = Security.getProvider( - System.getProperty("test.provider.name", "SunJCE")); - EncryptedPrivateKeyInfo ekpi = PEMDecoder.of().decode(encEdECKey, EncryptedPrivateKeyInfo.class); PrivateKey priKey = PEMDecoder.of().withDecryption(password). @@ -86,23 +67,11 @@ public class GetKey { + "match with expected."); } - // Test getKey(key, provider) provider null + // Test getKey(EdEC) if (!Arrays.equals(priKey.getEncoded(), - ekpi.getKey(key, null).getEncoded())) { - throw new AssertionError("getKey(key, provider) " + + ekpi.getKey(key).getEncoded())) { + throw new AssertionError("getKey(key) " + "didn't match with expected."); } - - // Test getKey(key, provider) with provider - EncryptedPrivateKeyInfo ekpiDH = PEMDecoder.of().decode(encDHECKey, - EncryptedPrivateKeyInfo.class); - PrivateKey priKeyDH = PEMDecoder.of().withDecryption(password). - decode(encDHECKey, PrivateKey.class); - - if (!Arrays.equals(priKeyDH.getEncoded(), - ekpiDH.getKey(key, p).getEncoded())) { - throw new AssertionError("getKey(key, provider) " + - "didn't match with expected."); - } } } diff --git a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java index d35197e1971..fd4df8366ab 100644 --- a/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java +++ b/test/jdk/javax/crypto/EncryptedPrivateKeyInfo/GetKeyPair.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -61,9 +61,6 @@ public class GetKeyPair { 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(); @@ -92,12 +89,8 @@ public class GetKeyPair { 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)); + System.out.print("Testing getKeyPair(key): "); + arrayCheck(ekpi.getKeyPair(key)); } static void arrayCheck(KeyPair kp) throws Exception { diff --git a/test/jdk/sun/security/provider/X509Factory/BadPem.java b/test/jdk/sun/security/provider/X509Factory/BadPem.java index c63f9295123..31e7a7e3a63 100644 --- a/test/jdk/sun/security/provider/X509Factory/BadPem.java +++ b/test/jdk/sun/security/provider/X509Factory/BadPem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -53,7 +53,7 @@ public class BadPem { pass.toCharArray()); byte[] cert = keyStore.getCertificate(alias).getEncoded(); - // 8074935 + // Revert 8074935. RFC 7468 allows non-base64 chars ByteArrayOutputStream bout = new ByteArrayOutputStream(); PrintStream pout = new PrintStream(bout); byte[] CRLF = new byte[] {'\r', '\n'}; @@ -67,9 +67,8 @@ public class BadPem { try { cf.generateCertificate(new ByteArrayInputStream(bout.toByteArray())); - throw new Exception("Should fail"); } catch (CertificateException e) { - // Good + throw new Exception("Should not fail"); } // 8208602 diff --git a/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilder.java b/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilder.java index 91ea3533cb8..9c68ae138ef 100644 --- a/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilder.java +++ b/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,7 +49,7 @@ * @author Xuelei Fan */ -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.Security; @@ -340,11 +340,11 @@ public class CPBuilder { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); for (String key : certmap.keySet()) { String certStr = certmap.get(key); - DEREncodable cert = PEM_DECODER.decode(certStr, X509Certificate.class); + BinaryEncodable cert = PEM_DECODER.decode(certStr, X509Certificate.class); entries.add(cert); } diff --git a/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilderWithMD5.java b/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilderWithMD5.java index 036a9634469..cbdeb6609e4 100644 --- a/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilderWithMD5.java +++ b/test/jdk/sun/security/provider/certpath/DisabledAlgorithms/CPBuilderWithMD5.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -54,7 +54,7 @@ * certificates used in this test are generated by an updated generate.sh that * replacing MD2 with MD5 algorithm. */ -import java.security.DEREncodable; +import java.security.BinaryEncodable; import java.security.PEMDecoder; import java.util.*; import java.security.cert.*; @@ -344,14 +344,14 @@ public class CPBuilderWithMD5 { } private static CertStore generateCertificateStore() throws Exception { - Collection entries = new HashSet<>(); + Collection entries = new HashSet<>(); // generate certificate from certificate string CertificateFactory cf = CertificateFactory.getInstance("X.509"); for (String key : certmap.keySet()) { String certStr = certmap.get(key); - DEREncodable cert = PEM_DECODER.decode(certStr, X509Certificate.class); + BinaryEncodable cert = PEM_DECODER.decode(certStr, X509Certificate.class); entries.add(cert); } diff --git a/test/jdk/sun/security/rsa/pss/PSSKeyCompatibility.java b/test/jdk/sun/security/rsa/pss/PSSKeyCompatibility.java index 940d4bd2059..49ee7387f17 100644 --- a/test/jdk/sun/security/rsa/pss/PSSKeyCompatibility.java +++ b/test/jdk/sun/security/rsa/pss/PSSKeyCompatibility.java @@ -76,7 +76,7 @@ public class PSSKeyCompatibility { try { final PEMDecoder decoder = PEMDecoder.of() - .withFactory(Security.getProvider(provider)); + .withFactoriesOf(Security.getProvider(provider)); final PrivateKey priv = decoder.decode( type, PrivateKey.class @@ -119,7 +119,7 @@ public class PSSKeyCompatibility { System.out.println(cert); final PEMDecoder decoder = PEMDecoder.of() - .withFactory(Security.getProvider(provider)); + .withFactoriesOf(Security.getProvider(provider)); final RSAPublicKey pub = decoder.decode( PEMEncoder.of().encodeToString( new X509EncodedKeySpec(