jwtk/jjwt · error · io.jsonwebtoken.security.SignatureException
Ciphertext decryption failed: Authentication tag verificatio
Error message
Ciphertext decryption failed: Authentication tag verification failed.
What it means
HmacAesAeadAlgorithm (A128GCM/A192GCM/A256GCM JWE content encryption) authenticates ciphertext with an HMAC-derived tag; on decrypt it recomputes the tag and compares in constant time. A mismatch means the ciphertext, IV, AAD, or tag was altered or the key is wrong, so decryption fails fast with a SignatureException instead of returning garbage plaintext.
Source
Thrown at impl/src/main/java/io/jsonwebtoken/impl/security/HmacAesAeadAlgorithm.java:200
InputStream in = Assert.notNull(req.getPayload(),
"Decryption request content (ciphertext) InputStream cannot be null.");
final InputStream aad = req.getAssociatedData(); // can be null if there's no associated data
final byte[] tag = assertTag(req.getDigest());
final byte[] iv = assertDecryptionIv(req);
final AlgorithmParameterSpec ivSpec = getIvSpec(iv);
// Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided,
// thereby verifying none of it has been tampered with:
byte[] aadBytes = aad == null ? Bytes.EMPTY : Streams.bytes(aad, "Unable to read AAD bytes.");
byte[] digest;
try {
digest = sign(aadBytes, iv, in, macKeyBytes);
} finally {
Bytes.clear(macKeyBytes);
}
if (!MessageDigest.isEqual(digest, tag)) { //constant time comparison to avoid side-channel attacks
String msg = "Ciphertext decryption failed: Authentication tag verification failed.";
throw new SignatureException(msg);
}
Streams.reset(in); // rewind for decryption
final InputStream ciphertext = in;
jca(req).withCipher(new CheckedFunction<Cipher, byte[]>() {
@Override
public byte[] apply(Cipher cipher) throws Exception {
cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec);
withCipher(cipher, ciphertext, plaintext);
return Bytes.EMPTY;
}
});
}
}
View on GitHub (pinned to fb71496164)
Solutions
- Verify both sides use the identical secret key (compare key bytes/IDs out-of-band).
- Inspect the token end-to-end transport for truncation or re-encoding; base64url must remain intact.
- Re-encrypt and resend the token if corruption in transit is suspected — do not attempt to repair.
- If this happens unexpectedly with attacker-controlled input, treat it as tampering and audit the source.
Example fix
// before: decrypting with a stale rotated key Jwe<Claims> jwe = Jwts.parser().decryptWith(oldSecretKey).build().parseEncryptedClaims(token); // after SecretKey currentKey = loadCurrentKey(); Jwe<Claims> jwe = Jwts.parser().decryptWith(currentKey).build().parseEncryptedClaims(token);
Defensive patterns
Strategy: try-catch
Try / catch
try {
Jwe<Claims> jwe = Jwts.parser().decryptWith(secretKey).build().parseEncryptedClaims(token);
} catch (SignatureException e) {
// authentication tag mismatch: key wrong or token tampered/corrupted
auditLog.recordTamperAttempt(token);
} Prevention
- Keep encryption/decryption keys in sync (same key store, rotation plan)
- Transmit tokens in base64url-safe channels; never re-encode
- Treat every tag failure as possible tampering and log it
- Prefer AAD-protected tokens produced and parsed by the same library version
When it happens
Trigger: Decrypting a JWE whose authentication tag doesn't verify: wrong decryption key, tampered or truncated compact token, ciphertext/IV/tag fields corrupted during transport, or re-encoding the token with a different character set.
Common situations: Storing tokens in systems that mangle base64url (e.g. leading '=' padding added); shared secrets differing between encryptor and decryptor (env-specific keys); attempting to decrypt with an older rotated key; malicious modification of the token.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Compact JWE strings must always contain an AAD Authenticatio
- Compact JWE strings must always contain an Initialization Ve
- Unexpected content JWE.
- Unexpected Claims JWE.
- PrivateKeys may not be used to encrypt data. PublicKeys are
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/56ad45ba027e0dd8.
Report an issue: GitHub.