jwtk/jjwt · error · InvalidKeyException
PublicKeys may not be used to decrypt data. PublicKeys are u
Error message
PublicKeys may not be used to decrypt data. PublicKeys are used to encrypt, and PrivateKeys are used to decrypt.
What it means
Asymmetric JWE encryption uses the recipient's public key to encrypt and the private key to decrypt. If the key locator resolves a PublicKey for decryption, the parser throws InvalidKeyException with PUB_KEY_DECRYPT_MSG because a public key cannot perform cipher decryption.
Source
Thrown at impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java:546
buf.get(aadBytes);
InputStream aad = Streams.of(aadBytes);
base64Url = base64UrlDigest;
//guaranteed to be non-empty via the `alg` + digest check above:
Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty.");
digest = decode(base64Url, "JWE AAD Authentication Tag");
if (Bytes.isEmpty(digest)) {
String msg = "Compact JWE strings must always contain an AAD Authentication Tag.";
throw new MalformedJwtException(msg);
}
Key key = this.keyLocator.locate(jweHeader);
if (key == null) {
String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader;
throw new UnsupportedJwtException(msg);
}
if (key instanceof PublicKey) {
throw new InvalidKeyException(PUB_KEY_DECRYPT_MSG);
}
// extract key-specific provider if necessary;
Provider provider = ProviderKey.getProvider(key, this.provider);
key = ProviderKey.getKey(key); // this must be called after ProviderKey.getProvider
DecryptionKeyRequest<Key> request =
new DefaultDecryptionKeyRequest<>(cekBytes, provider, null, jweHeader, encAlg, key);
final SecretKey cek = keyAlg.getDecryptionKey(request);
if (cek == null) {
String msg = "The '" + keyAlg.getId() + "' JWE key algorithm did not return a decryption key. " +
"Unable to perform '" + encAlg.getId() + "' decryption.";
throw new IllegalStateException(msg);
}
// During decryption, the available Provider applies to the KeyAlgorithm, not the AeadAlgorithm, mostly
// because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath).
// As such, the provider here is intentionally omitted (null):
// TODO: add encProvider(Provider) builder method that applies to this request only?View on GitHub (pinned to fb71496164)
Solutions
- Use the recipient's PrivateKey for decryption: point keyLocator/decryptWith at the private key that pairs with the encrypting public key.
- Separate locators: one returning public keys for verification/encryption, one returning private keys for decryption.
- Check asymmetric setup — if you are the sender you should encrypt with the recipient's PublicKey, not decrypt.
- Ensure your key store loader returns the PrivateKey entry, not its certificate's public key.
Example fix
// before parser.keyLocator(h -> keyPair.getPublic()); // after parser.keyLocator(h -> keyPair.getPrivate());
Defensive patterns
Strategy: type-guard
Validate before calling
Key k = locator.locate(header);
if (k instanceof java.security.PublicKey) throw new IllegalStateException("need private key for JWE decryption"); Type guard
boolean canDecrypt(Key k) { return k instanceof PrivateKey || k instanceof SecretKey; } Try / catch
try { parser.parse(jwe); } catch (InvalidKeyException e) { log.error("PublicKey used for decryption — check key wiring"); } Prevention
- Keep separate locators/configs for encrypt (PublicKey) and decrypt (PrivateKey) flows.
- Name your key configuration explicitly (e.g. recipientPrivateKey) to avoid copy-paste mixups.
- When loading from keystores, fetch PrivateKey entries, not certificate public keys.
- Add startup assertions that decryption keys are PrivateKey/SecretKey instances.
When it happens
Trigger: Parsing/decrypting a JWE where the configured keyLocator (or key set) returns a PublicKey instance, e.g. accidentally wiring the verification/encryption key pair in the wrong direction.
Common situations: Reusing the same keyLocator for both signing-verification and decryption, loading JWKS public keys for a flow that needs your own private key, copy-pasting the sender's configuration.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Invalid RSA key algorithm name.
- RSASSA-PSS keys may not be used for ${keyType}, only digital
- Unable to determine JWA-standard Elliptic Curve for ${type}k
- JWE Header epk value is not an Elliptic Curve Public JWK. Va
- JWE Header epk value does not represent a point on the expec
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/13e3fef59ad02a25.
Report an issue: GitHub.