Skip to content

HMAC Generator — Kotlin 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 Kotlin 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: Kotlin 1.9 (JVM, standard library + javax.crypto)
// 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.
//
// Kotlin/JVM has no crypto in kotlin-stdlib, so the idiomatic path is the
// JDK's javax.crypto.Mac — the same vetted provider set the Java port of
// this tool uses. All four "HmacSHA*" names are standard JCA algorithm
// strings; no external dependencies are needed.
//
// 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 Kotlin standing of the Python port's
// ValueError and the two arms of the Rust port's HmacError enum (Mac would
// accept a zero-length key; SubtleCrypto refuses one, and the ports keep
// parity). SHA-1 is offered for legacy compatibility only; it is not
// collision-resistant.

import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

/** Pure logic of the CosmoDev `hmac-generator` tool. */
object HmacTool {

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

    /** Every supported algorithm, in the order the React island renders them. */
    val ALGORITHMS: List<String> = listOf(SHA1, SHA256, SHA384, SHA512)

    /** Dispatch table: canonical name -> JCA Mac algorithm string (the same
     * shape as the Python port's _HASH_CONSTRUCTORS). buildMap keeps
     * insertion order through LinkedHashMap, exactly like the Java port. */
    private val JCA_NAMES: Map<String, String> = buildMap {
        put(SHA1, "HmacSHA1")
        put(SHA256, "HmacSHA256")
        put(SHA384, "HmacSHA384")
        put(SHA512, "HmacSHA512")
    }

    /**
     * Computes HMAC([message], [secret]) under the named algorithm and
     * returns it as lowercase hex.
     *
     * Both inputs are UTF-8 encoded before hashing, so the result is correct
     * for arbitrary Unicode (emoji, accents, CJK). A null or empty
     * [algorithm] selects SHA-256 — the default parameter the TypeScript
     * reference declares. An empty [secret] or an unknown name throws
     * [IllegalArgumentException].
     */
    fun hmacHex(message: String, secret: String, algorithm: String = SHA256): String {
        val name = algorithm.takeIf { it.isNotEmpty() } ?: SHA256

        // Mac would accept a zero-length key; SubtleCrypto refuses one.
        require(secret.isNotEmpty()) { "HMAC secret must not be empty" }

        val jcaName = JCA_NAMES[name]
            ?: throw IllegalArgumentException("Unsupported HMAC algorithm: $name")

        // A fresh instance per call: Mac is stateful. SecretKeySpec wraps the
        // UTF-8 key bytes — the same byte sequence a browser hands to
        // crypto.subtle.sign with an HmacImportParams key.
        return try {
            val mac = Mac.getInstance(jcaName)
            mac.init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), jcaName))
            val tag = mac.doFinal(message.toByteArray(Charsets.UTF_8))

            // Two lowercase hex digits per byte — %02x formats a Byte as its
            // unsigned two-digit hex value.
            tag.joinToString(separator = "") { "%02x".format(it) }
        } catch (e: NoSuchAlgorithmException) {
            // Unreachable on a conforming JDK: the four HmacSHA* names are
            // mandatory JCA algorithms.
            throw IllegalArgumentException("Unsupported HMAC algorithm: $name", e)
        } catch (e: InvalidKeyException) {
            // 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 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 →