HMAC in Java: Message Authentication Codes with HmacSHA256

HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function to produce a tag that proves both integrity and authenticity. Unlike a plain hash, an attacker without the key cannot forge an HMAC. Java exposes HMAC through the javax.crypto.Mac class.

Share

HMAC vs Plain Hash: Why a Key Matters

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

Computing HMAC-SHA256

Java
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.

Generating a Secure HMAC Key

Java
// 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));

Constant-Time Verification: Preventing Timing Attacks

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

Java
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.

Real-World Example: Webhook Signature Verification

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

Java
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=<hex>
     */
    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);
    }
}

API Request Signing Pattern

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

Java
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));
    }
}

HMAC Algorithm Variants

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
Java
// 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