Hash Type Identifier — C# 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 C# implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Hash-type identifier — C# port.
//
// Language: C# (C# 12 / .NET 8, 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.
#nullable enable
namespace CosmoDev.HashTypeIdentifier;
/// <summary>The character set classification of a candidate hash string.</summary>
public enum HashCharset
{
Hex,
Base64,
Bcrypt,
Argon2,
Unknown,
}
/// <summary>
/// Lowercase identifier matching the TypeScript string literal used by the
/// canonical implementation (so serialised output agrees).
/// </summary>
public static class HashCharsetExtensions
{
public static string AsLabel(this HashCharset charset) => charset switch
{
HashCharset.Hex => "hex",
HashCharset.Base64 => "base64",
HashCharset.Bcrypt => "bcrypt",
HashCharset.Argon2 => "argon2",
_ => "unknown",
};
}
/// <summary>A candidate hash algorithm and its nominal bit length.</summary>
/// <param name="Name">Algorithm family name.</param>
/// <param name="BitLength">Hex length * 4, where applicable.</param>
public readonly record struct HashMatch(string Name, long BitLength);
/// <summary>The full identification result for an input string.</summary>
public sealed record HashInfo(
string Input,
string Cleaned,
int Length,
HashCharset Charset,
IReadOnlyList<HashMatch> Candidates);
public static class HashIdentify
{
// 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 static readonly Dictionary<int, string[]> HexByLength = new()
{
[8] = ["CRC32", "Adler-32"],
[16] = ["MySQL 3.x", "CRC64"],
[32] = ["MD5", "MD4", "NTLM", "LM", "MD2", "RIPEMD-128", "HAVAL-128"],
[40] = ["SHA-1", "RIPEMD-160", "HAVAL-160", "MySQL 5.x (SHA1(SHA1))", "Tiger-160"],
[56] = ["SHA-224", "SHA3-224", "BLAKE2s-224", "HAVAL-224"],
[64] = ["SHA-256", "SHA3-256", "BLAKE2s-256", "RIPEMD-256", "Skein-256"],
[96] = ["SHA-384", "SHA3-384", "BLAKE2b-384"],
[128] = ["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 static readonly Dictionary<int, string[]> Base64ByLength = new()
{
[24] = ["MD5 (base64)"],
[28] = ["SHA-1 (base64)"],
[44] = ["SHA-256 (base64)"],
[88] = ["SHA-512 (base64)"],
};
/// <summary>
/// Matches the bcrypt modular-crypt prefix <c>^\$2[abxy]?\$</c> — prefix
/// match only; the variable trailing payload is not inspected.
/// </summary>
private static bool LooksLikeBcrypt(ReadOnlySpan<char> s)
{
if (!s.StartsWith("$2") || s.Length < 3)
return false;
return s[2] switch
{
'a' or 'b' or 'x' or 'y' => s.Length >= 4 && s[3] == '$',
'$' => true,
_ => false,
};
}
/// <summary>Matches the argon2 modular-crypt prefix
/// <c>^\$argon2(id|i|d)?\$</c>.</summary>
private static bool LooksLikeArgon2(ReadOnlySpan<char> s)
{
const string prefix = "$argon2";
if (!s.StartsWith(prefix))
return false;
ReadOnlySpan<char> rest = s[prefix.Length..];
// Try the two-char variant first so `id` wins over the bare `i`.
if (rest.StartsWith("id"))
return rest.Length >= 3 && rest[2] == '$';
if (rest.IsEmpty)
return false;
if (rest[0] is 'i' or 'd')
return rest.Length >= 2 && rest[1] == '$';
return rest[0] == '$';
}
/// <summary>Whole-string hex match, mirroring the <c>+</c> quantifier
/// (non-empty body).</summary>
private static bool LooksLikeHex(ReadOnlySpan<char> s)
{
if (s.IsEmpty)
return false;
foreach (char c in s)
if (!char.IsAsciiHexDigit(c))
return false;
return true;
}
/// <summary>
/// Valid standard-alphabet base64 with 0–2 trailing <c>=</c> padding;
/// the body before padding must be non-empty.
/// </summary>
private static bool LooksLikeBase64(ReadOnlySpan<char> s)
{
// Strip up to two trailing `=` padding characters, then require the
// remaining body to be non-empty and entirely base64 alphabet bytes.
int end = s.Length, pad = 0;
while (end > 0 && s[end - 1] == '=' && pad < 2)
{
end--;
pad++;
}
if (end == 0)
return false;
foreach (char c in s[..end])
if (!(char.IsAsciiLetterOrDigit(c) || c == '+' || c == '/'))
return false;
return true;
}
/// <summary>Classify the charset of a candidate hash string.</summary>
/// <remarks>
/// 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.
/// </remarks>
public static HashCharset DetectCharset(ReadOnlySpan<char> s)
{
if (LooksLikeBcrypt(s)) return HashCharset.Bcrypt;
if (LooksLikeArgon2(s)) return HashCharset.Argon2;
if (LooksLikeHex(s)) return HashCharset.Hex;
if (LooksLikeBase64(s)) return HashCharset.Base64;
return HashCharset.Unknown;
}
/// <summary>
/// Identify candidate hash types for an input string.
/// </summary>
/// <remarks>
/// Always returns a fully populated <see cref="HashInfo"/>; never
/// throws. An empty, unrecognised, or wrong-length input simply yields
/// an empty candidate list — the caller decides whether "no candidates"
/// means "not a hash".
/// </remarks>
public static HashInfo IdentifyHash(string? input)
{
string original = input ?? "";
string cleaned = original.Trim();
int length = cleaned.Length;
HashCharset charset = DetectCharset(cleaned);
List<HashMatch> candidates = [];
switch (charset)
{
case HashCharset.Bcrypt:
// bcrypt's modular-crypt token encodes a 184-bit effective hash.
candidates.Add(new HashMatch("bcrypt", 184));
break;
case HashCharset.Argon2:
// Argon2 output length is parameter-driven, so no fixed bit
// length applies.
candidates.Add(new HashMatch("Argon2", 0));
break;
case HashCharset.Hex:
if (HexByLength.TryGetValue(length, out string[]? hexNames))
{
// length*4 converts hex-char count to a bit width
// (4 bits per nibble).
long bits = (long)length * 4;
foreach (string name in hexNames)
candidates.Add(new HashMatch(name, bits));
}
break;
case HashCharset.Base64:
if (Base64ByLength.TryGetValue(length, out string[]? b64Names))
{
// Each base64 char carries 6 bits; round to the nearest
// byte boundary. All table lengths divide evenly, so the
// integer division below is exact.
long bits = length * 6 / 8 * 8;
foreach (string name in b64Names)
candidates.Add(new HashMatch(name, bits));
}
break;
}
return new HashInfo(original, cleaned, length, charset, 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 →