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 →