Skip to content

HMAC Generator — Swift 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 Swift 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: Swift 5.9 (Apple CryptoKit / swift-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.
//
// The TypeScript reference delegates to `crypto.subtle.sign` with an HMAC
// key, and the Rust port uses the RustCrypto `hmac`/`sha1`/`sha2` crates.
// The Swift standard library ships no crypto, so the idiomatic path is
// Apple's CryptoKit — and on non-Apple platforms the open-source
// swift-crypto package, which exposes the same HMAC/SHA types under
// `import Crypto` (the ecosystem equivalent named where Rust names its
// crates). The canImport switch below picks the right module either way.
//
// Behavior mirrors the TypeScript reference: UTF-8 inputs, lowercase hex
// output, SHA-256 by default, and rejection of empty secrets and unknown
// algorithms (HmacError, the Swift standing of the Rust port's HmacError
// enum). The empty-secret case matters for parity: HMAC's authenticationCode
// would accept a zero-length SymmetricKey, but SubtleCrypto refuses one.
// SHA-1 is offered for legacy compatibility only; it is not
// collision-resistant.

import Foundation

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto // swift-crypto on Linux
#endif

/// Canonical algorithm names. The spellings match the TypeScript union so the
/// same string works across every port. The case order matches the order the
/// React island renders.
public enum HmacAlgorithm: String, CaseIterable, Sendable {
    case sha1 = "SHA-1"
    case sha256 = "SHA-256"
    case sha384 = "SHA-384"
    case sha512 = "SHA-512"

    /// The algorithm selected when a caller passes an empty name — the
    /// optional-parameter default the TypeScript reference declares.
    public static let `default`: HmacAlgorithm = .sha256
}

/// Errors thrown by `hmacHex`. A concrete enum (not a string) so callers can
/// switch on the kind — the same shape as the Rust port's HmacError.
public enum HmacError: Error, Equatable, Sendable {
    /// The algorithm string was not one of the supported names.
    case unknownAlgorithm(String)
    /// An empty secret was supplied; the TypeScript reference rejects this too.
    case emptySecret
}

/// Lowercase hexadecimal encoding of a byte sequence — Foundation-free, the
/// Swift shape of the Rust port's `to_hex`.
private func toHex<S: Sequence>(_ bytes: S) -> String where S.Element == UInt8 {
    let digits = Array("0123456789abcdef".unicodeScalars)
    var out = String.UnicodeScalarView()
    for byte in bytes {
        out.append(digits[Int(byte >> 4)])
        out.append(digits[Int(byte & 0x0f)])
    }
    return String(out)
}

/// Computes HMAC(`message`, `secret`) under the named algorithm and returns
/// it as lowercase hex.
///
/// Both inputs are UTF-8 encoded before hashing (String.utf8), so the result
/// is correct for arbitrary Unicode (emoji, accents, CJK). Passing an empty
/// `algorithm` selects SHA-256; an empty `secret` throws
/// `HmacError.emptySecret`, and an unknown name throws
/// `HmacError.unknownAlgorithm`.
public func hmacHex(_ message: String, secret: String, algorithm: String = "") throws -> String {
    guard !secret.isEmpty else {
        throw HmacError.emptySecret
    }

    // A String's utf8 view is the exact byte sequence crypto.subtle.sign
    // receives from TextEncoder in the TypeScript reference.
    let key = SymmetricKey(data: Data(secret.utf8))
    let data = Data(message.utf8)

    switch algorithm {
    case HmacAlgorithm.sha1.rawValue:
        // CryptoKit keeps SHA-1 behind the Insecure namespace (as does
        // swift-crypto) to signal its broken collision resistance.
        return toHex(HMAC<Insecure.SHA1>.authenticationCode(for: data, using: key))
    case HmacAlgorithm.sha256.rawValue, "":
        return toHex(HMAC<SHA256>.authenticationCode(for: data, using: key))
    case HmacAlgorithm.sha384.rawValue:
        return toHex(HMAC<SHA384>.authenticationCode(for: data, using: key))
    case HmacAlgorithm.sha512.rawValue:
        return toHex(HMAC<SHA512>.authenticationCode(for: data, using: key))
    default:
        throw HmacError.unknownAlgorithm(algorithm)
    }
}

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 →