JEP 538 in Java 27: PEM Encodings of Cryptographic Objects
-
Last Updated: September 28, 2026
-
By: javahandson
-
Series

Learn JEP 538 in Java 27: encode and decode PEM keys and certificates with PEMEncoder and PEMDecoder, encrypt private keys, and avoid common mistakes.
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:
Let us get started.
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.
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 label tells you what is inside. The Base64 body carries the real details, such as the algorithm and the key bits.
You will meet these labels most often:
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.
PEM files show up in almost every backend project. For example:
In short, PEM is the common language of keys and certificates. So Java really needed first-class support for it.
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.
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:
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.
The PEM API did not arrive in one step. It has moved through three rounds of preview:
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.
If you tried the API in JDK 25 or 26, a few names have changed. Here are the main updates in JEP 538:
| 💡 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. |
The API is small. You mainly need to know six types. Most of them live in the java.security package.
BinaryEncodable is a sealed interface. It marks every object that the PEM API can encode or decode. In JDK 27, these types implement it:
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.
PEMEncoder turns an object into PEM text. It has only four methods:
PEMDecoder does the reverse job. It reads PEM text and gives you back a Java object. Its API is also small:
Both PEMEncoder and PEMDecoder are immutable and thread-safe. So you can create them once and share them across your application.
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.
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.
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.
Now comes the fun part. Let us write real code for the common tasks. All examples run on JDK 27 with preview enabled.
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.
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.
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. |
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.
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.
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.
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.
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 footerOne 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.
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.
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.
Good error handling makes a big difference in production. Here is a quick map of what can go wrong:
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. |
Let me share a few lessons that will save you some debugging time.
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:
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.
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.
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.
A: Use PEMEncoder.of().withEncryption(password).encodeToString(privateKey). The output uses the ENCRYPTED PRIVATE KEY label, and the default algorithm is PBEWithHmacSHA256AndAES_128.
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.
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.
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:
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.