Skip to content

HMAC Generator — Java source

Generate a keyed-hash HMAC (SHA-1/256/384/512) for a message and secret. Runs entirely in your browser via Web Crypto, with a shareable link to your exact input.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// hmac-generator — RFC 2104 keyed-hash HMAC of a UTF-8 message, hex output.
//
// Language: Java 17 (standard library only)
// Source:   CosmoDev polyglot showcase port of the `hmac-generator` tool,
//           ported from src/lib/hmac.ts (the canonical TypeScript lib).
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Keyed HMAC via javax.crypto.Mac — the JDK's vetted provider set (typically
// backed by OpenSSL through the SunJCE native patches). All four "HmacSHA*"
// names are standard JCA algorithm strings, so no external libraries are
// needed. Hex encoding uses Character.forDigit — no formatter and no
// third-party Hex class.
//
// Behavior mirrors the TypeScript reference: UTF-8 inputs, lowercase hex
// output, SHA-256 by default, and rejection of empty secrets and unknown
// algorithms. An empty secret and an unknown name both throw
// IllegalArgumentException — the Java standing of the ValueError the Python
// port raises and the two arms of the Rust port's HmacError enum. The empty
// secret matters for parity: Mac would accept a zero-length key, but the
// TypeScript reference (SubtleCrypto) refuses one. SHA-1 is offered for
// legacy compatibility only; it is not collision-resistant.

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/** Pure logic of the CosmoDev {@code hmac-generator} tool. */
public final class HmacTool {

    /** Canonical algorithm names. The spellings match the TypeScript union so
     * the same string works across every port. */
    public static final String SHA1 = "SHA-1";
    public static final String SHA256 = "SHA-256"; // default algorithm
    public static final String SHA384 = "SHA-384";
    public static final String SHA512 = "SHA-512";

    /** The algorithm selected when a caller passes null or "" — the
     * optional-parameter default the TypeScript reference declares. */
    public static final String DEFAULT_ALGORITHM = SHA256;

    /** Every supported algorithm, in the order the React island renders them. */
    public static final List<String> ALGORITHMS = List.of(SHA1, SHA256, SHA384, SHA512);

    /** Dispatch table: canonical name -> JCA Mac algorithm string. Module-level
     * so the supported set is visible in one place (the same shape as the
     * Python port's _HASH_CONSTRUCTORS). */
    private static final Map<String, String> JCA_NAMES = new LinkedHashMap<>();
    static {
        JCA_NAMES.put(SHA1, "HmacSHA1");
        JCA_NAMES.put(SHA256, "HmacSHA256");
        JCA_NAMES.put(SHA384, "HmacSHA384");
        JCA_NAMES.put(SHA512, "HmacSHA512");
    }

    private HmacTool() {
        // Static utility — no instances.
    }

    /**
     * Computes HMAC({@code message}, {@code secret}) under the named
     * algorithm and returns it as lowercase hex.
     *
     * <p>Both inputs are UTF-8 encoded before hashing, so the result is
     * correct for arbitrary Unicode (emoji, accents, CJK). A null or empty
     * {@code algorithm} selects SHA-256. An empty {@code secret} or an
     * unknown algorithm name throws {@link IllegalArgumentException}.
     */
    public static String hmacHex(String message, String secret, String algorithm) {
        String name = (algorithm == null || algorithm.isEmpty()) ? DEFAULT_ALGORITHM : algorithm;

        if (secret == null || secret.isEmpty()) {
            throw new IllegalArgumentException("HMAC secret must not be empty");
        }

        String jcaName = JCA_NAMES.get(name);
        if (jcaName == null) {
            throw new IllegalArgumentException("Unsupported HMAC algorithm: " + name);
        }

        // A fresh instance per call: Mac is stateful, and reusing one across
        // tags would need reset() bookkeeping. SecretKeySpec wraps the UTF-8
        // key bytes — the same byte sequence a browser hands to
        // crypto.subtle.sign with an HmacImportParams key.
        try {
            Mac mac = Mac.getInstance(jcaName);
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), jcaName));
            byte[] tag = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));

            // Two hex digits per byte, lowercase — Character.forDigit already
            // yields lowercase for bases 16 and below.
            StringBuilder hex = new StringBuilder(tag.length * 2);
            for (byte b : tag) {
                hex.append(Character.forDigit((b >> 4) & 0xf, 16));
                hex.append(Character.forDigit(b & 0xf, 16));
            }
            return hex.toString();
        } catch (NoSuchAlgorithmException e) {
            // Unreachable on a conforming JDK: the four HmacSHA* names are
            // mandatory JCA algorithms. Translated for the same reason as
            // above — a bad name is a caller bug, not an environment failure.
            throw new IllegalArgumentException("Unsupported HMAC algorithm: " + name, e);
        } catch (InvalidKeyException e) {
            // Unreachable: HMAC accepts keys of any length per RFC 2104, and
            // the empty-secret guard above already rejected the one case
            // SecretKeySpec would refuse.
            throw new IllegalArgumentException("Invalid HMAC key", e);
        }
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →