Skip to content

Hash Type Identifier — Kotlin source

Identify the likely hash algorithm of a hash string by its length and character set - MD5, SHA-1/2/3, BLAKE, CRC32, NTLM, bcrypt, Argon2 and more.

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

// Hash-type identifier — Kotlin port.
//
// Language: Kotlin (1.9+, standard library only)
// Source:   CosmoDev polyglot showcase port of the `hash-type-identifier`
//           tool, ported from src/lib/hashIdentify.ts (the canonical
//           TypeScript implementation).
// License:  display source — part of CosmoDev's polyglot tool pages
//           (dev.cosmolabs.org).
//
// Pure string classification: inspect a candidate hash's charset and length
// to suggest likely algorithms. No hashing happens here — this is pattern
// recognition over an already-computed digest. Deterministic; never throws.

/** The character set classification of a candidate hash string. */
enum class HashCharset(val label: String) {
    HEX("hex"),
    BASE64("base64"),
    BCRYPT("bcrypt"),
    ARGON2("argon2"),
    UNKNOWN("unknown"),
}

/** A candidate hash algorithm and its nominal bit length
 *  (hex length * 4, where applicable). */
data class HashMatch(val name: String, val bitLength: Long)

/** The full identification result for an input string. */
data class HashInfo(
    val input: String,
    val cleaned: String,
    val length: Int,
    val charset: HashCharset,
    val candidates: List<HashMatch> = emptyList(),
)

/** Hex candidates keyed by hex-string length. Each hex char encodes 4 bits,
 *  so a 64-char digest implies a 256-bit algorithm such as SHA-256. */
private val HEX_BY_LENGTH: Map<Int, List<String>> = mapOf(
    8 to listOf("CRC32", "Adler-32"),
    16 to listOf("MySQL 3.x", "CRC64"),
    32 to listOf("MD5", "MD4", "NTLM", "LM", "MD2", "RIPEMD-128", "HAVAL-128"),
    40 to listOf("SHA-1", "RIPEMD-160", "HAVAL-160", "MySQL 5.x (SHA1(SHA1))", "Tiger-160"),
    56 to listOf("SHA-224", "SHA3-224", "BLAKE2s-224", "HAVAL-224"),
    64 to listOf("SHA-256", "SHA3-256", "BLAKE2s-256", "RIPEMD-256", "Skein-256"),
    96 to listOf("SHA-384", "SHA3-384", "BLAKE2b-384"),
    128 to listOf("SHA-512", "SHA3-512", "BLAKE2b-512", "Whirlpool", "Skein-512"),
)

/** Base64 candidates keyed by encoded-string length (16-byte MD5 digest ->
 *  24 base64 chars including padding, etc.). */
private val BASE64_BY_LENGTH: Map<Int, List<String>> = mapOf(
    24 to listOf("MD5 (base64)"),
    28 to listOf("SHA-1 (base64)"),
    44 to listOf("SHA-256 (base64)"),
    88 to listOf("SHA-512 (base64)"),
)

private fun Char.isAsciiHexDigit(): Boolean =
    this in '0'..'9' || this in 'a'..'f' || this in 'A'..'F'

private fun Char.isAsciiBase64(): Boolean =
    this in '0'..'9' || this in 'A'..'Z' || this in 'a'..'z' || this == '+' || this == '/'

/**
 * Matches the bcrypt modular-crypt prefix `^\$2[abxy]?\$` — prefix match
 * only; the variable trailing payload is not inspected.
 */
private fun looksLikeBcrypt(s: String): Boolean {
    if (!s.startsWith("$2") || s.length < 3) return false
    return when (s[2]) {
        'a', 'b', 'x', 'y' -> s.length >= 4 && s[3] == '$'
        '$' -> true
        else -> false
    }
}

/** Matches the argon2 modular-crypt prefix `^\$argon2(id|i|d)?\$`. */
private fun looksLikeArgon2(s: String): Boolean {
    if (!s.startsWith("\$argon2")) return false
    val rest = s.substring("\$argon2".length)
    // Try the two-char variant first so `id` wins over the bare `i`.
    if (rest.startsWith("id")) return rest.length >= 3 && rest[2] == '$'
    return when (rest.firstOrNull()) {
        'i', 'd' -> rest.length >= 2 && rest[1] == '$'
        '$' -> true
        else -> false
    }
}

/** Whole-string hex match, mirroring the `+` quantifier (non-empty body). */
private fun looksLikeHex(s: String): Boolean =
    s.isNotEmpty() && s.all { it.isAsciiHexDigit() }

/**
 * Valid standard-alphabet base64 with 0–2 trailing `=` padding; the body
 * before padding must be non-empty.
 */
private fun looksLikeBase64(s: String): Boolean {
    // Strip up to two trailing `=` padding characters, then require the
    // remaining body to be non-empty and entirely base64 alphabet chars.
    var end = s.length
    var pad = 0
    while (end > 0 && s[end - 1] == '=' && pad < 2) {
        end--
        pad++
    }
    if (end == 0) return false
    for (i in 0 until end) {
        if (!s[i].isAsciiBase64()) return false
    }
    return true
}

/**
 * Classify the charset of a candidate hash string.
 *
 * Order matters: hex is checked before base64 because every hex digest is
 * also a legal base64 character set, and the more specific classification
 * should win.
 */
fun detectCharset(s: String): HashCharset = when {
    looksLikeBcrypt(s) -> HashCharset.BCRYPT
    looksLikeArgon2(s) -> HashCharset.ARGON2
    looksLikeHex(s) -> HashCharset.HEX
    looksLikeBase64(s) -> HashCharset.BASE64
    else -> HashCharset.UNKNOWN
}

/**
 * Identify candidate hash types for an input string.
 *
 * Always returns a fully-populated [HashInfo]; never throws. A null input is
 * treated as the empty string. An empty, unrecognised, or wrong-length input
 * simply yields an empty candidate list — the caller decides whether "no
 * candidates" means "not a hash".
 */
fun identifyHash(input: String?): HashInfo {
    val original = input.orEmpty()
    val cleaned = original.trim()
    val charset = detectCharset(cleaned)
    val length = cleaned.length

    val candidates = when (charset) {
        HashCharset.BCRYPT ->
            // bcrypt's modular-crypt token encodes a 184-bit effective hash.
            listOf(HashMatch("bcrypt", 184))
        HashCharset.ARGON2 ->
            // Argon2 output length is parameter-driven, so no fixed bit
            // length applies.
            listOf(HashMatch("Argon2", 0))
        HashCharset.HEX ->
            // length*4 converts hex-char count to a bit width (4 bits per
            // nibble).
            HEX_BY_LENGTH[length].orEmpty().map { HashMatch(it, length.toLong() * 4) }
        HashCharset.BASE64 ->
            // Each base64 char carries 6 bits; round to the nearest byte
            // boundary. All table lengths divide evenly, so the integer
            // division is exact.
            BASE64_BY_LENGTH[length].orEmpty()
                .map { HashMatch(it, (length * 6 / 8).toLong() * 8) }
        HashCharset.UNKNOWN -> emptyList()
    }

    return HashInfo(
        input = original,
        cleaned = cleaned,
        length = length,
        charset = charset,
        candidates = candidates,
    )
}

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 →