PropertyPlain Hash (SHA-256)HMAC-SHA256
InputMessage onlyMessage + secret key
Integrity✅ Detects accidental corruption✅ Detects corruption
Authenticity❌ Anyone can compute it✅ Only key holders can compute it
Forgeable without keyYesNo
Length-extension attackVulnerable (SHA-256)Not vulnerable
Use casesChecksums, deduplicationAPI signing, webhook verification, JWT HS256
import javax.crypto.*; import javax.crypto.spec.*; import java.security.*; import java.util.Base64; import java.util.HexFormat; public class HmacExample { public static byte[] hmacSha256(byte[] data, byte[] keyBytes) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); return mac.doFinal(data); } public static void main(String[] args) throws Exception { byte[] key = "super-secret-key-at-least-32-bytes".getBytes(StandardCharsets.UTF_8); byte[] message = "Hello, HMAC!".getBytes(StandardCharsets.UTF_8); byte[] tag = hmacSha256(message, key); System.out.println("Hex : " + HexFormat.of().formatHex(tag)); System.out.println("Base64: " + Base64.getEncoder().encodeToString(tag)); } } The HMAC key should be at least as long as the hash output — 32 bytes for HMAC-SHA256, 64 bytes for HMAC-SHA512. Use KeyGenerator or SecureRandom to generate key material; never use a low-entropy password directly. // Generate a secure random 256-bit HMAC key KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256"); kg.init(256); // key length in bits SecretKey key = kg.generateKey(); byte[] keyBytes = key.getEncoded(); // store these bytes securely // Reconstruct from stored bytes SecretKey restoredKey = new SecretKeySpec(keyBytes, "HmacSHA256"); // Use Mac mac = Mac.getInstance("HmacSHA256"); mac.init(restoredKey); byte[] tag = mac.doFinal("message".getBytes(StandardCharsets.UTF_8));

Comparing HMAC tags with Arrays.equals() leaks timing information — an attacker can determine how many bytes matched. Always use constant-time comparison.

import java.security.MessageDigest; /** * Verify an HMAC tag in constant time. * Returns true only if the computed tag exactly matches the expected tag. */ public static boolean verify(byte[] message, byte[] expectedTag, byte[] keyBytes) throws Exception { byte[] computed = hmacSha256(message, keyBytes); // MessageDigest.isEqual is constant-time return MessageDigest.isEqual(computed, expectedTag); } // Usage byte[] received = Base64.getDecoder().decode(receivedTagBase64); if (!verify(message, received, secretKey)) { throw new SecurityException("HMAC verification failed — message tampered or wrong key"); } Never use Arrays.equals(tag1, tag2) or tag1.equals(tag2) to compare HMAC tags. They short-circuit on the first mismatch, leaking timing information. MessageDigest.isEqual() always compares all bytes.

Many services (Stripe, GitHub, Twilio) send an HMAC signature in a request header so you can verify the payload came from them.

import javax.crypto.*; import javax.crypto.spec.*; import java.security.*; import java.util.HexFormat; public class WebhookVerifier { private final byte[] secret; public WebhookVerifier(String webhookSecret) { this.secret = webhookSecret.getBytes(StandardCharsets.UTF_8); } /** * Verify a GitHub-style webhook signature. * Header: X-Hub-Signature-256: sha256= */ public boolean verify(byte[] payload, String signatureHeader) throws Exception { if (signatureHeader == null || !signatureHeader.startsWith("sha256=")) { return false; } String expectedHex = signatureHeader.substring(7); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret, "HmacSHA256")); byte[] computed = mac.doFinal(payload); byte[] expected = HexFormat.of().parseHex(expectedHex); return MessageDigest.isEqual(computed, expected); } }

Sign API requests to prevent replay attacks and ensure the request body was not tampered with in transit.

import java.time.Instant; import java.util.Base64; public class ApiSigner { private final byte[] signingKey; public ApiSigner(byte[] signingKey) { this.signingKey = signingKey; } /** * Create a signed request string. * Format: METHOD\nPATH\nTIMESTAMP\nBODY_HASH */ public String sign(String method, String path, String body) throws Exception { String timestamp = String.valueOf(Instant.now().getEpochSecond()); String bodyHash = sha256Hex(body.getBytes(StandardCharsets.UTF_8)); String toSign = method + "\n" + path + "\n" + timestamp + "\n" + bodyHash; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(signingKey, "HmacSHA256")); byte[] tag = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); // Attach timestamp + signature as request headers return "t=" + timestamp + ",v1=" + Base64.getEncoder().encodeToString(tag); } private String sha256Hex(byte[] data) throws Exception { return HexFormat.of().formatHex( MessageDigest.getInstance("SHA-256").digest(data)); } }
AlgorithmJCA nameTag sizeUse when
HMAC-SHA256HmacSHA25632 bytesDefault — general purpose
HMAC-SHA384HmacSHA38448 bytesStronger security margin
HMAC-SHA512HmacSHA51264 bytesHigh-security contexts; faster on 64-bit
HMAC-SHA3-256HmacSHA3-25632 bytesPost-quantum readiness (Java 9+)
HMAC-MD5HmacMD516 bytesLegacy only — avoid for new code
// HMAC-SHA512 Mac mac = Mac.getInstance("HmacSHA512"); mac.init(new SecretKeySpec(keyBytes, "HmacSHA512")); byte[] tag = mac.doFinal(data); System.out.println("Tag length: " + tag.length + " bytes"); // 64