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

  1. Verify both sides use the identical secret key (compare key bytes/IDs out-of-band).
  2. Inspect the token end-to-end transport for truncation or re-encoding; base64url must remain intact.
  3. Re-encrypt and resend the token if corruption in transit is suspected — do not attempt to repair.
  4. 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

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

Related errors


AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09). Data as JSON: /api/errors/56ad45ba027e0dd8. Report an issue: GitHub.