Skip to content

Hash Type Identifier — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Hash-type identifier — Java port.
//
// Language: Java (Java 17, 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.

import java.util.List;
import java.util.Map;

/** Hash-type identification for the CosmoDev polyglot showcase. */
public final class HashIdentify {

    private HashIdentify() {}

    /** The character set classification of a candidate hash string. */
    public enum HashCharset {
        HEX("hex"),
        BASE64("base64"),
        BCRYPT("bcrypt"),
        ARGON2("argon2"),
        UNKNOWN("unknown");

        private final String label;

        HashCharset(String label) {
            this.label = label;
        }

        /**
         * Lowercase identifier matching the TypeScript string literal used by
         * the canonical implementation (so serialised output agrees).
         */
        public String label() {
            return label;
        }
    }

    /** A candidate hash algorithm and its nominal bit length
     *  (hex length * 4, where applicable). */
    public record HashMatch(String name, long bitLength) {}

    /** The full identification result for an input string. */
    public record HashInfo(
            String input,
            String cleaned,
            int length,
            HashCharset charset,
            List<HashMatch> candidates) {}

    /**
     * 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 final Map<Integer, List<String>> HEX_BY_LENGTH = Map.of(
            8,   List.of("CRC32", "Adler-32"),
            16,  List.of("MySQL 3.x", "CRC64"),
            32,  List.of("MD5", "MD4", "NTLM", "LM", "MD2", "RIPEMD-128", "HAVAL-128"),
            40,  List.of("SHA-1", "RIPEMD-160", "HAVAL-160", "MySQL 5.x (SHA1(SHA1))", "Tiger-160"),
            56,  List.of("SHA-224", "SHA3-224", "BLAKE2s-224", "HAVAL-224"),
            64,  List.of("SHA-256", "SHA3-256", "BLAKE2s-256", "RIPEMD-256", "Skein-256"),
            96,  List.of("SHA-384", "SHA3-384", "BLAKE2b-384"),
            128, List.of("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 final Map<Integer, List<String>> BASE64_BY_LENGTH = Map.of(
            24, List.of("MD5 (base64)"),
            28, List.of("SHA-1 (base64)"),
            44, List.of("SHA-256 (base64)"),
            88, List.of("SHA-512 (base64)"));

    /**
     * Matches the bcrypt modular-crypt prefix {@code ^\$2[abxy]?\$} — prefix
     * match only; the variable trailing payload is not inspected.
     */
    private static boolean looksLikeBcrypt(String s, int n) {
        if (n < 3 || s.charAt(0) != '$' || s.charAt(1) != '2') {
            return false;
        }
        char variant = s.charAt(2);
        if (variant == 'a' || variant == 'b' || variant == 'x' || variant == 'y') {
            return n >= 4 && s.charAt(3) == '$';
        }
        return variant == '$';
    }

    /** Matches the argon2 modular-crypt prefix {@code ^\$argon2(id|i|d)?\$}. */
    private static boolean looksLikeArgon2(String s, int n) {
        final int prefix = "$argon2".length();
        if (n <= prefix || !s.startsWith("$argon2")) {
            return false;
        }
        // Try the two-char variant first so `id` wins over the bare `i`.
        if (n - prefix >= 2 && s.charAt(prefix) == 'i' && s.charAt(prefix + 1) == 'd') {
            return n - prefix >= 3 && s.charAt(prefix + 2) == '$';
        }
        char variant = s.charAt(prefix);
        if (variant == 'i' || variant == 'd') {
            return n - prefix >= 2 && s.charAt(prefix + 1) == '$';
        }
        return variant == '$';
    }

    /**
     * Whole-string hex match, mirroring the {@code +} quantifier
     * (non-empty body of ASCII hex digits).
     */
    private static boolean looksLikeHex(String s, int n) {
        if (n == 0) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (!isAsciiHexDigit(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Valid standard-alphabet base64 with 0–2 trailing {@code =} padding;
     * the body before padding must be non-empty.
     */
    private static boolean looksLikeBase64(String s, int n) {
        int end = n;
        int pad = 0;
        while (end > 0 && s.charAt(end - 1) == '=' && pad < 2) {
            end--;
            pad++;
        }
        if (end == 0) {
            return false;
        }
        for (int i = 0; i < end; i++) {
            if (!isAsciiBase64(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private static boolean isAsciiHexDigit(char c) {
        return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    }

    private static boolean isAsciiBase64(char c) {
        return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')
            || (c >= 'a' && c <= 'z') || c == '+' || c == '/';
    }

    /**
     * Classify the charset of a candidate hash string.
     *
     * <p>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.
     */
    public static HashCharset detectCharset(String s) {
        int n = s.length();
        if (looksLikeBcrypt(s, n)) {
            return HashCharset.BCRYPT;
        }
        if (looksLikeArgon2(s, n)) {
            return HashCharset.ARGON2;
        }
        if (looksLikeHex(s, n)) {
            return HashCharset.HEX;
        }
        if (looksLikeBase64(s, n)) {
            return HashCharset.BASE64;
        }
        return HashCharset.UNKNOWN;
    }

    /**
     * Identify candidate hash types for an input string.
     *
     * <p>Always returns a fully populated {@link 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".
     */
    public static HashInfo identifyHash(String input) {
        String original = input == null ? "" : input;
        String cleaned = original.strip();
        HashCharset charset = detectCharset(cleaned);
        int length = cleaned.length();

        List<HashMatch> candidates;
        switch (charset) {
            case BCRYPT -> {
                // bcrypt's modular-crypt token encodes a 184-bit effective hash.
                candidates = List.of(new HashMatch("bcrypt", 184));
            }
            case ARGON2 -> {
                // Argon2 output length is parameter-driven, so no fixed bit
                // length applies.
                candidates = List.of(new HashMatch("Argon2", 0));
            }
            case HEX -> {
                List<String> names = HEX_BY_LENGTH.get(length);
                if (names == null) {
                    candidates = List.of();
                } else {
                    // length*4 converts hex-char count to a bit width
                    // (4 bits per nibble).
                    long bits = (long) length * 4;
                    candidates = names.stream()
                            .map(name -> new HashMatch(name, bits))
                            .toList();
                }
            }
            case BASE64 -> {
                List<String> names = BASE64_BY_LENGTH.get(length);
                if (names == null) {
                    candidates = List.of();
                } else {
                    // 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 = (long) (length * 6 / 8) * 8;
                    candidates = names.stream()
                            .map(name -> new HashMatch(name, bits))
                            .toList();
                }
            }
            default -> candidates = List.of();
        }

        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 →