| Property | Plain Hash (SHA-256) | HMAC-SHA256 |
| Input | Message only | Message + secret key |
| Integrity | ✅ Detects accidental corruption | ✅ Detects corruption |
| Authenticity | ❌ Anyone can compute it | ✅ Only key holders can compute it |
| Forgeable without key | Yes | No |
| Length-extension attack | Vulnerable (SHA-256) | Not vulnerable |
| Use cases | Checksums, deduplication | API 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));
}
}
| Algorithm | JCA name | Tag size | Use when |
| HMAC-SHA256 | HmacSHA256 | 32 bytes | Default — general purpose |
| HMAC-SHA384 | HmacSHA384 | 48 bytes | Stronger security margin |
| HMAC-SHA512 | HmacSHA512 | 64 bytes | High-security contexts; faster on 64-bit |
| HMAC-SHA3-256 | HmacSHA3-256 | 32 bytes | Post-quantum readiness (Java 9+) |
| HMAC-MD5 | HmacMD5 | 16 bytes | Legacy 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