When implementing JWT authentication or cryptographic signing in a Java or Spring Boot application, you may encounter:
java.lang.IllegalArgumentException: Illegal base64 character 2d
This error often appears when Java tries to load a private key stored in PEM format.
The good news is that the private key may be perfectly valid. The problem is usually how the PEM value is being decoded.
Security note: Never publish real private keys in source code, logs, tickets, documentation, or blog posts. Always use anonymized examples.
Understanding the Error
A typical private key looks like this:
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
...
-----END PRIVATE KEY-----
A common mistake is:
byte[] decoded = Base64.getDecoder().decode(privateKey);
The problem is that the complete PEM document is not Base64.
Only the content between:
-----BEGIN PRIVATE KEY-----
and:
-----END PRIVATE KEY-----
is Base64 encoded.
Why Does Java Say 2d?
This is actually a useful clue.
Hexadecimal:
2D
represents the ASCII character:
-
And what does a PEM file start with?
-----BEGIN PRIVATE KEY-----
^
The Base64 decoder encounters the - character from the PEM header and rejects it.
So:
Illegal base64 character 2d
often means:
You’re trying to Base64-decode the PEM header together with the actual key.
The Correct Solution
Remove the PEM header, footer, and whitespace before decoding:
String privateKeyContent = privateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decodedKey =
Base64.getDecoder().decode(privateKeyContent);
For a PKCS#8 RSA key, you can then create the Java private key:
PKCS8EncodedKeySpec keySpec =
new PKCS8EncodedKeySpec(decodedKey);
KeyFactory keyFactory =
KeyFactory.getInstance("RSA");
PrivateKey rsaPrivateKey =
keyFactory.generatePrivate(keySpec);
A reusable helper could therefore look like:
public static PrivateKey loadPrivateKey(String pem)
throws GeneralSecurityException {
String content = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded =
Base64.getDecoder().decode(content);
PKCS8EncodedKeySpec spec =
new PKCS8EncodedKeySpec(decoded);
return KeyFactory
.getInstance("RSA")
.generatePrivate(spec);
}
PKCS#8 vs PKCS#1
Pay attention to the PEM header.
If you have:
-----BEGIN PRIVATE KEY-----
the key is normally PKCS#8, which works with:
PKCS8EncodedKeySpec
However, if you have:
-----BEGIN RSA PRIVATE KEY-----
you are usually dealing with PKCS#1.
You cannot necessarily remove the header and pass a PKCS#1 key directly to:
PKCS8EncodedKeySpec
The key may first need to be converted or parsed with a library that supports PKCS#1.
Base64 vs Base64URL
JWT applications can make this even more confusing.
Java provides:
Base64.getDecoder()
for standard Base64 and:
Base64.getUrlDecoder()
for Base64URL.
JWT components use Base64URL, but a PEM private key normally contains standard Base64.
Therefore, don’t switch to:
Base64.getUrlDecoder()
just because Java complains about a -.
If the - belongs to:
-----BEGIN PRIVATE KEY-----
the PEM header is the real problem.
Spring Boot, Docker, and Kubernetes
Another common source of problems is passing private keys through configuration:
Secret
↓
Environment variable
↓
Spring property
↓
Java String
↓
Base64 decoder
Before decoding, determine what the application actually receives.
It could be:
- the complete PEM document;
- only the Base64 body;
- Base64 encoding of the entire PEM document;
- a PKCS#1 key;
- a PKCS#8 key.
Kubernetes can make this especially confusing because Secret values are themselves commonly represented as Base64.
You can therefore accidentally create multiple encoding layers.
Debugging Safely
Never do this:
log.info("Private key: {}", privateKey);
Instead, log only structural information:
log.debug(
"Key length: {}, PEM header detected: {}",
privateKey.length(),
privateKey.contains("BEGIN PRIVATE KEY")
);
This lets you investigate the problem without exposing sensitive cryptographic material.
Quick Troubleshooting Checklist
If you encounter:
Illegal base64 character 2d
check:
- Does the value contain
-----BEGIN PRIVATE KEY-----? - Are you removing the PEM header and footer?
- Are spaces and line breaks removed before decoding?
- Are you using
Base64.getDecoder()rather than the JWT Base64URL decoder? - Is the key PKCS#8 or PKCS#1?
- Does the selected Java
KeySpecmatch the key format? - Has the key been Base64-encoded more than once by configuration or Kubernetes?
Conclusion
The exception:
java.lang.IllegalArgumentException:
Illegal base64 character 2d
usually doesn’t mean the private key is corrupted.
With PEM keys, 2d often points directly to the - character in:
-----BEGIN PRIVATE KEY-----
The correct processing flow is:
PEM → remove header/footer → remove whitespace → Base64 decode → create KeySpec → create PrivateKey
Understanding the difference between PEM, Base64, Base64URL, PKCS#1, and PKCS#8 makes these Java cryptography problems much easier to diagnose.
SEO Elements
SEO Title: Java Illegal Base64 Character 2d: Fix PEM Private Key Errors
Meta Description: Learn why Java throws “Illegal base64 character 2d” when loading PEM private keys and how to correctly handle Base64, PKCS#8, RSA, JWT, and Spring Boot.
Focus Keyphrase: Java illegal base64 character 2d
Suggested Slug: java-illegal-base64-character-2d-private-key
Keywords: Java Base64 error, Java PEM private key, Spring Boot JWT, PKCS8EncodedKeySpec, PKCS#8 Java, PKCS#1 Java, RSA private key, JWT authentication, Java cryptography
WordPress Tags: Java, Spring Boot, JWT, Base64, PEM, RSA, PKCS8, Private Key, Cryptography, Java Security


