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 →