Skip to content

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 →