JEP 538 in Java 27: PEM Encodings of Cryptographic Objects

  • Last Updated: September 28, 2026
  • By: javahandson
  • Series
img

JEP 538 in Java 27: PEM Encodings of Cryptographic Objects

Learn JEP 538 in Java 27: encode and decode PEM keys and certificates with PEMEncoder and PEMDecoder, encrypt private keys, and avoid common mistakes.

1. Introduction

If you have ever loaded a key from a .pem file in Java, you know the pain. JEP 538 gives Java 27 a clean, built-in way to read and write PEM text. In this guide, we will learn the new PEM API step by step, with simple and working examples.

Before this API, Java had no easy way to handle PEM. So most of us wrote string-trimming helpers or pulled in Bouncy Castle just for this one job. I have done the same in many banking integrations. Partner banks almost always share their keys and certificates as PEM files.

Java 27 reached general availability on 15 September 2026. JEP 538 ships in it as a third preview. That means the API is nearly final, but you must switch on preview features to use it.

By the end of this guide, you will know:

  • what PEM is and why it shows up everywhere
  • how we handled PEM before, and why it hurt
  • what changed in this third preview
  • how to use PEMEncoder, PEMDecoder and the PEM class
  • how to encrypt and decrypt private keys with a password
  • common mistakes and a few best practices

Let us get started.

2. What Is PEM?

PEM stands for Privacy-Enhanced Mail. It was first designed to send cryptographic data inside email. Email could not carry raw binary data safely, so the data had to become plain text.

The secure mail idea never really took off. However, the text format survived and spread everywhere. Today, RFC 7468 defines the textual encoding that most tools follow.

2.1 How a PEM Text Looks

Here is a real elliptic curve (EC) public key in PEM format. I generated it with OpenSSL for this article:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/OQVKbPnid6ni2eR2tIacevglF1Y
xG+0w5NerEnlFJQ/owXVF9I8tsqm2vSkA4qB5ncLWVJjsjk0DbcMjyk4PA==
-----END PUBLIC KEY-----

A PEM text has three simple parts:

  • The header line has BEGIN and a type label, like PUBLIC KEY.
  • Next comes a body of Base64 text. This body holds the binary form of the object.
  • Finally, the footer line has END and the same label.

The label tells you what is inside. The Base64 body carries the real details, such as the algorithm and the key bits.

2.2 Common PEM Types

You will meet these labels most often:

  • CERTIFICATE for X.509 certificates
  • PUBLIC KEY for public keys
  • PRIVATE KEY for unencrypted PKCS#8 private keys
  • ENCRYPTED PRIVATE KEY for password-protected PKCS#8 private keys
  • X509 CRL for certificate revocation lists
  • CERTIFICATE REQUEST for certificate signing requests (CSRs)

You may also see RSA PRIVATE KEY. Older OpenSSL versions write RSA keys in this PKCS#1 style. Good news: the JDK implementation can decode the unencrypted form of it too.

2.3 Where You Meet PEM in Real Projects

PEM files show up in almost every backend project. For example:

  • TLS certificates for your web server or API gateway
  • mTLS setups, where both client and server show certificates
  • public keys that a partner shares to verify signed messages
  • JWT signing keys stored in a vault or a Kubernetes secret
  • keys created with OpenSSL on the command line

In short, PEM is the common language of keys and certificates. So Java really needed first-class support for it.

3. The Problem Before JEP 538

Java has always had strong crypto classes. Every key and certificate can give you its binary form. The Base64 class can then turn that binary into text. However, all the steps in between were left to us.

3.1 Decoding a Public Key the Old Way

Here is the classic code that many projects still carry today:

String base64 = pemText
        .replace("-----BEGIN PUBLIC KEY-----", "")
        .replace("-----END PUBLIC KEY-----", "")
        .replaceAll("\\s", "");
 
byte[] der = Base64.getDecoder().decode(base64);
X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
 
// You must already know the algorithm here
KeyFactory factory = KeyFactory.getInstance("EC");
PublicKey publicKey = factory.generatePublic(spec);

This works, but it has several weak spots:

  • The string cleanup breaks easily. One extra comment line and the parsing fails.
  • You must know the key algorithm up front, such as RSA or EC.
  • You need a different KeySpec class for public and private keys.
  • Every team writes its own helper, with its own small bugs.

3.2 Encrypted Private Keys Were Even Worse

Password-protected private keys needed much more work. You had to parse the encrypted structure and derive a key from the password. Then you set up a Cipher and finally built the private key. The JEP itself notes that this took over a dozen lines of code.

As a result, many teams simply added Bouncy Castle for its PEM classes. That library works well, but it is a heavy dependency for such a common task.

The pain was real, too. Oracle’s Java Cryptographic Extensions Survey in April 2022 confirmed it as a common complaint.

4. The Journey of the PEM API

The PEM API did not arrive in one step. It has moved through three rounds of preview:

  • JDK 25: JEP 470 introduced the first preview.
  • JDK 26: JEP 524 brought a second preview. For instance, PEMRecord became PEM, and KeyPair support arrived.
  • JDK 27: JEP 538 is the third preview, which is our topic today.

There is good news for the future as well. JEP 542 proposes to make the API final in JDK 28. It also plans to finalize it without any further change. So what you learn today should carry over directly.

5. What Changed in the Third Preview

If you tried the API in JDK 25 or 26, a few names have changed. Here are the main updates in JEP 538:

  • DEREncodable is now BinaryEncodable. The new name describes the binary data better. It is also not limited to DER encoding.
  • PEM is now a normal class. Earlier it was a record. Now it has constructors that accept Base64 content as byte arrays. It also makes defensive copies of the data.
  • withFactory() is now withFactoriesOf(). The new name makes it clear that key and certificate factories come from the given provider.
  • Simpler EncryptedPrivateKeyInfo methods. The getKey() and getKeyPair() versions that took a password and a Provider now take only a Key.
  • A new CryptoException. This unchecked exception reports failures during cryptographic work, such as a failed decryption.
💡 Interview Insight
An interviewer may ask why DEREncodable got a new name. DER is only one binary encoding rule. The interface really stands for anything with binary key or certificate material. So BinaryEncodable is the more honest name. It also leaves room for new types in future releases.

6. The Core Building Blocks

The API is small. You mainly need to know six types. Most of them live in the java.security package.

6.1 BinaryEncodable

BinaryEncodable is a sealed interface. It marks every object that the PEM API can encode or decode. In JDK 27, these types implement it:

  • AsymmetricKey, which covers PublicKey and PrivateKey
  • KeyPair
  • X509Certificate and X509CRL
  • X509EncodedKeySpec and PKCS8EncodedKeySpec
  • EncryptedPrivateKeyInfo
  • PEM

Note that the permits list is marked as not exhaustive. More types may join later. We will see why this matters for switch statements soon.

6.2 PEMEncoder

PEMEncoder turns an object into PEM text. It has only four methods:

  • of() returns an encoder.
  • encode() returns the PEM as a byte array.
  • encodeToString() returns the PEM as a String.
  • withEncryption(char[]) returns a new encoder that encrypts private keys.

6.3 PEMDecoder

PEMDecoder does the reverse job. It reads PEM text and gives you back a Java object. Its API is also small:

  • of() returns the default decoder.
  • decode(String) and decode(InputStream) return a BinaryEncodable.
  • decode(String, Class) and decode(InputStream, Class) return the exact type you ask for.
  • withDecryption(char[]) returns a decoder that can decrypt private keys.
  • withFactoriesOf(Provider) returns a decoder that uses a specific provider.

Both PEMEncoder and PEMDecoder are immutable and thread-safe. So you can create them once and share them across your application.

6.4 PEM

The PEM class covers any PEM text that has no matching Java class. A certificate signing request is a good example. It holds three things: the type label, the Base64 content and any leading data before the header.

6.5 EncryptedPrivateKeyInfo and CryptoException

EncryptedPrivateKeyInfo is an old class from javax.crypto. It now implements BinaryEncodable and gains new encrypt(), getKey() and getKeyPair() methods. You use it when you need full control over encryption.

CryptoException is brand new in javax.crypto. It extends RuntimeException, so the compiler does not force you to catch it. The JDK uses it for crypto failures in places where checked exceptions would feel awkward.

7. Enabling Preview Features

Since this is a preview API, the compiler blocks it by default. You need the –enable-preview flag at compile time and at run time.

For a single file, run it directly with the source launcher:

java --enable-preview --source 27 PemDemo.java

For a normal compile and run, use these two commands:

javac --release 27 --enable-preview PemDemo.java
java --enable-preview PemDemo

With Maven, add the flag to the compiler plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <release>27</release>
        <compilerArgs>
            <arg>--enable-preview</arg>
        </compilerArgs>
    </configuration>
</plugin>

Also pass –enable-preview to the Surefire plugin through argLine if your tests use the API. Otherwise, the JVM refuses to load your classes and throws an UnsupportedClassVersionError.

8. Hands-On Examples

Now comes the fun part. Let us write real code for the common tasks. All examples run on JDK 27 with preview enabled.

8.1 Encode a Key Pair to PEM

First, let us generate an EC key pair and turn both keys into PEM text:

import java.security.*;
 
public class PemEncodeDemo {
 
    public static void main(String[] args) throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
        generator.initialize(256);
        KeyPair keyPair = generator.generateKeyPair();
 
        PEMEncoder encoder = PEMEncoder.of();
 
        String publicPem = encoder.encodeToString(keyPair.getPublic());
        String privatePem = encoder.encodeToString(keyPair.getPrivate());
 
        System.out.println(publicPem);
        System.out.println(privatePem);
    }
}

The public key prints with a PUBLIC KEY header. Similarly, the private key prints with a PRIVATE KEY header. You never touch Base64 or header strings yourself.

You can even pass the whole KeyPair to the encoder. In that case, the output still uses the PRIVATE KEY label. Inside, it follows the PKCS#8 v2.0 structure that holds both keys together.

8.2 Decode PEM into a Specific Type

Next, let us read that PEM text back. When you know what to expect, pass the class you want:

PEMDecoder decoder = PEMDecoder.of();
 
PublicKey publicKey = decoder.decode(publicPem, PublicKey.class);
PrivateKey privateKey = decoder.decode(privatePem, PrivateKey.class);

Compare this with the old code in section 3.1. There is no string cleanup, no KeySpec and no KeyFactory. Moreover, you do not need to know that the key is EC. The decoder reads the algorithm from the data itself.

What if the class does not match the content? Suppose you ask for a PrivateKey, but the text holds a certificate. In that case, the decoder throws a ClassCastException.

8.3 Decode Unknown PEM with Pattern Matching

Sometimes you do not know what a file contains. For example, an admin may upload some key file through a UI. In that case, decode it without a class and use a pattern matching switch:

BinaryEncodable decoded = PEMDecoder.of().decode(pemText);
 
switch (decoded) {
    case PublicKey key -> System.out.println("Public key: " + key.getAlgorithm());
    case PrivateKey key -> System.out.println("Private key: " + key.getAlgorithm());
    case KeyPair pair -> System.out.println("Key pair found");
    case X509Certificate cert ->
            System.out.println("Certificate: " + cert.getSubjectX500Principal());
    case PEM pem -> System.out.println("No Java class for: " + pem.type());
    default -> throw new IllegalArgumentException("Unexpected content");
}

Two things are worth noticing here. First, any PEM type without a Java class comes back as a PEM object. Second, the default branch is required.

💡 Interview Insight
A common question is: “BinaryEncodable is sealed, so why do we still need a default branch?” The answer lies in its permits list, which is marked as not exhaustive. The JDK may add new subtypes in future releases. Therefore, listing today’s subtypes does not make your switch complete. A default or case BinaryEncodable label keeps old code working when new types arrive.

8.4 Read a Certificate from a File

Most real PEM data lives in files. The decoder can read straight from an InputStream:

PEMDecoder decoder = PEMDecoder.of();
 
try (InputStream in = Files.newInputStream(Path.of("partner-cert.pem"))) {
    X509Certificate cert = decoder.decode(in, X509Certificate.class);
    System.out.println("Subject: " + cert.getSubjectX500Principal());
    System.out.println("Expires: " + cert.getNotAfter());
}

The stream version reads only up to the end of one PEM footer. Because of this, you can call it again to read the next block. That is handy for a certificate chain file:

List<X509Certificate> chain = new ArrayList<>();
 
try (InputStream in = Files.newInputStream(Path.of("chain.pem"))) {
    while (true) {
        try {
            chain.add(decoder.decode(in, X509Certificate.class));
        } catch (EOFException end) {
            break; // no more PEM blocks
        }
    }
}

When no more PEM data remains, the decoder throws an EOFException. We simply use that as our exit signal.

8.5 Encrypt a Private Key with a Password

Storing a plain private key on disk is risky. Anyone who reads the file owns the key. So let us protect it with a password:

char[] password = readPasswordFromVault(); // never hardcode it
 
String encryptedPem = PEMEncoder.of()
        .withEncryption(password)
        .encodeToString(keyPair.getPrivate());

The output now starts with an ENCRYPTED PRIVATE KEY header. Behind the scenes, the encoder picks the default password-based algorithm. In JDK 27, that default is PBEWithHmacSHA256AndAES_128. The jdk.epkcs8.defaultAlgorithm security property controls it.

An encrypting encoder only accepts PrivateKey, KeyPair and PKCS8EncodedKeySpec objects. If you pass a public key or a certificate, it throws an IllegalArgumentException. That makes sense, since public data needs no secret protection.

8.6 Decrypt an Encrypted Private Key

Reading the key back is just as short:

PrivateKey privateKey = PEMDecoder.of()
        .withDecryption(password)
        .decode(encryptedPem, PrivateKey.class);

If the password is wrong, the decoder throws a CryptoException. Also, a decoder with decryption turned on can still read plain, unencrypted PEM. So one configured decoder can handle both kinds of files.

What if you decode encrypted text with a plain decoder? It does not fail. Instead, it hands you an EncryptedPrivateKeyInfo object that you can decrypt later:

EncryptedPrivateKeyInfo info = PEMDecoder.of()
        .decode(encryptedPem, EncryptedPrivateKeyInfo.class);
 
PrivateKey privateKey = info.getKey(password);

This helps when the password arrives later than the file. For example, you might load files at startup and fetch the password on first use.

8.7 Custom Encryption Settings

The default algorithm is fine for most apps. However, your security team may ask for a stronger one. In that case, call EncryptedPrivateKeyInfo.encrypt() and then encode the result:

EncryptedPrivateKeyInfo info = EncryptedPrivateKeyInfo.encrypt(
        keyPair.getPrivate(),
        password,
        "PBEWithHmacSHA512AndAES_256",
        null,   // use the provider's default parameters
        null);  // use the default provider list
 
String encryptedPem = PEMEncoder.of().encodeToString(info);

The algorithm name and its parameters travel inside the encrypted data. So a decoder can always decrypt it later, even if the JDK default changes in a future release.

8.8 Handle Unsupported Types with the PEM Class

Java has no class for a certificate signing request. Still, the PEM API can carry it for you:

PEM csr = PEMDecoder.of().decode(csrText, PEM.class);
 
System.out.println(csr.type());   // CERTIFICATE REQUEST
byte[] der = csr.decode();        // raw binary bytes

The type() method gives the label. Meanwhile, decode() returns the Base64-decoded bytes, which you can pass to another library.

You can also build a PEM object yourself and turn it into text:

PEM pem = new PEM("CERTIFICATE REQUEST", base64Body);
String text = pem.toString(); // full text with header and footer

One more tip. By default, the decoder ignores any text before the PEM header. If you need that text, decode to PEM.class and call leadingData(). It returns the bytes that came before the header, or null if there were none.

8.9 Use a Specific Provider

Sometimes you must use a particular security provider, such as Bouncy Castle or a FIPS-certified one. The withFactoriesOf() method handles this:

Provider provider = Security.getProvider("BC"); // must be registered first
 
PEMDecoder decoder = PEMDecoder.of().withFactoriesOf(provider);
PrivateKey key = decoder.decode(privatePem, PrivateKey.class);

The decoder then takes its KeyFactory and CertificateFactory from that provider. Any provider errors show up during decoding, not during setup.

8.10 A Real-World Example: Verify a Partner’s Signature

Let me tie this back to banking work. A partner bank signs each callback it sends you. It also shares its public key once, as a PEM file. Your job is to verify every incoming message.

With the new API, the whole flow stays short and readable:

PEMDecoder decoder = PEMDecoder.of();
 
String pemText = Files.readString(Path.of("partner-public.pem"));
PublicKey partnerKey = decoder.decode(pemText, PublicKey.class);
 
Signature verifier = Signature.getInstance("SHA256withECDSA");
verifier.initVerify(partnerKey);
verifier.update(payload.getBytes(StandardCharsets.UTF_8));
 
boolean valid = verifier.verify(Base64.getDecoder().decode(signatureHeader));

In real code, load the key once at startup and keep it in memory. After that, each request only runs the Signature part. Also, reject the message right away when valid is false.

9. Exceptions You Should Know

Good error handling makes a big difference in production. Here is a quick map of what can go wrong:

  • IllegalArgumentException: the text has no PEM data, the PEM cannot be parsed, or you try to encode an unsupported object.
  • ClassCastException: the class you asked for does not match the PEM type.
  • CryptoException: encryption or decryption failed, for example because of a wrong password.
  • IOException and EOFException: problems while reading from an InputStream.
  • NullPointerException: you passed a null input or a null password.

Most of these are unchecked. Therefore, the compiler will not remind you about them. In a service that accepts uploaded keys, catch them at the boundary and return a clear error message.

💡 Interview Insight
Interviewers may ask why the new API throws an unchecked CryptoException instead of a GeneralSecurityException. The JEP describes CryptoException as a tool for unrecoverable failures, in places where checked exceptions are not wanted. A wrong password or a broken key usually cannot be fixed by retrying in code. So forcing every caller to catch a checked exception would add noise without adding safety.

10. Best Practices and Common Mistakes

Let me share a few lessons that will save you some debugging time.

10.1 Best Practices

  • Create PEMEncoder and PEMDecoder once and reuse them. They are immutable and thread-safe.
  • Prefer the typed decode methods when you know the content. They make your intent clear and avoid casts.
  • Keep passwords in char[] instead of String. The API clones the array, so clear your own copy with Arrays.fill() after use.
  • Always encrypt private keys that you store on disk or in shared storage.
  • Load passwords from a vault or a secret manager, never from source code.
  • Log the PEM type label on failure. Never log the Base64 body of a private key.

10.2 Common Mistakes

  • Forgetting –enable-preview at run time. The code compiles fine, but the class fails to load.
  • Using the old names from JDK 25 or 26, such as DEREncodable or withFactory(). They do not exist in JDK 27.
  • Skipping the default branch in a switch over BinaryEncodable. The compiler rejects it.
  • Passing a public key to an encrypting encoder. Only private key material can be encrypted.
  • Expecting an error when you decode encrypted PEM without a password. You get an EncryptedPrivateKeyInfo instead.
  • Shipping preview code to production without a plan. Preview APIs can still change, and you must rebuild for each new release.

10.3 Should You Replace Bouncy Castle Today?

This is a fair question, and my honest answer is: not yet in production. JDK 27 still marks the API as a preview. So it only runs with a special flag, and its names can still change.

However, the picture looks bright. JEP 542 plans to finalize the API in JDK 28 without further changes. Therefore, now is a good time to start experimenting.

Here is a simple plan that works well:

  • Write your new PEM helper against the JDK API in a test module.
  • Compare its output with your current Bouncy Castle code for the same keys.
  • Keep Bouncy Castle if you need extra formats, such as old OpenSSL encrypted keys.
  • Switch over once you move to a JDK release where the API is final.

11. FAQ’s on JEP 538 in Java 27

Q: What is JEP 538 in Java 27?

A: JEP 538 is the third preview of the PEM API in Java 27. It adds PEMEncoder, PEMDecoder, the PEM class and the BinaryEncodable interface, so you can convert keys, certificates and CRLs to and from PEM text without manual Base64 parsing.

Q: Do I need –enable-preview to use the PEM API in JDK 27?

A: Yes. In JDK 27 the PEM API is a preview API, so you must pass –enable-preview to both javac and java. JEP 542 proposes to make it final in JDK 28.

Q: How do I read a PEM public key in Java 27?

A: Call PEMDecoder.of().decode(pemText, PublicKey.class). The decoder reads the algorithm from the data, so you no longer need a KeySpec or a KeyFactory.

Q: How do I encrypt a private key as PEM with a password?

A: Use PEMEncoder.of().withEncryption(password).encodeToString(privateKey). The output uses the ENCRYPTED PRIVATE KEY label, and the default algorithm is PBEWithHmacSHA256AndAES_128.

Q: What changed in the third preview of the PEM API?

A: DEREncodable was renamed to BinaryEncodable, and PEM became a normal class with byte array constructors. withFactory() was renamed to withFactoriesOf(), the EncryptedPrivateKeyInfo getKey and getKeyPair methods now take a Key instead of a password and Provider, and a new unchecked CryptoException was added.

Q: Why does a switch over BinaryEncodable need a default branch?

A: BinaryEncodable is sealed, but its permits list is marked as not exhaustive. Future JDK releases may add new subtypes, so the switch needs a default or case BinaryEncodable label to stay complete.

13. Conclusion

JEP 538 finally gives Java a clean, built-in way to work with PEM. Encoding or decoding a key now takes one or two lines instead of fragile helper code.

Here is a quick recap:

  • PEM is Base64 text wrapped in BEGIN and END lines.
  • PEMEncoder turns keys, certificates and CRLs into PEM text.
  • PEMDecoder turns PEM text back into Java objects.
  • withEncryption() and withDecryption() handle password-protected private keys.
  • The PEM class carries types that have no Java class.
  • In JDK 27, you still need –enable-preview.

My advice is simple. Try the API now in a side project or a test module. Then, when JDK 28 makes it final, you can drop your old helper classes with confidence.

12. Further Reading

Leave a Comment