Skip to content

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

# frozen_string_literal: true

# Hash-type identifier — Ruby port.
#
# Language: Ruby (3.2+, 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 raises.

module HashTypeIdentifier
  # 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.
  HEX_BY_LENGTH = {
    8   => %w[CRC32 Adler-32],
    16  => ['MySQL 3.x', 'CRC64'],
    32  => %w[MD5 MD4 NTLM LM MD2 RIPEMD-128 HAVAL-128],
    40  => ['SHA-1', 'RIPEMD-160', 'HAVAL-160', 'MySQL 5.x (SHA1(SHA1))', 'Tiger-160'],
    56  => %w[SHA-224 SHA3-224 BLAKE2s-224 HAVAL-224],
    64  => %w[SHA-256 SHA3-256 BLAKE2s-256 RIPEMD-256 Skein-256],
    96  => %w[SHA-384 SHA3-384 BLAKE2b-384],
    128 => %w[SHA-512 SHA3-512 BLAKE2b-512 Whirlpool Skein-512],
  }.freeze

  # Base64 candidates keyed by encoded-string length (16-byte MD5 digest ->
  # 24 base64 chars including padding, etc.).
  BASE64_BY_LENGTH = {
    24 => ['MD5 (base64)'],
    28 => ['SHA-1 (base64)'],
    44 => ['SHA-256 (base64)'],
    88 => ['SHA-512 (base64)'],
  }.freeze

  # A candidate hash algorithm and its nominal bit length.
  HashMatch = Struct.new(:name, :bit_length, keyword_init: true)

  # The full identification result for an input string.
  HashInfo = Struct.new(:input, :cleaned, :length, :charset, :candidates, keyword_init: true)

  # Detection patterns. bcrypt and argon2 use their modular-crypt `$...$`
  # format, so they are matched by prefix (the trailing payload is variable);
  # the patterns carry no end anchor. hex and base64 match the whole string,
  # and hex is checked first because any hex digest is also a legal base64
  # character set.
  RE_BCRYPT = /\A\$2[abxy]?\$/
  RE_ARGON2 = /\A\$argon2(id|i|d)?\$/
  RE_HEX    = /\A[0-9a-fA-F]+\z/
  RE_BASE64 = /\A[A-Za-z0-9+\/]+={0,2}\z/

  module_function

  # Classify the charset of a candidate hash string.
  def detect_charset(s)
    return 'bcrypt' if RE_BCRYPT.match?(s)
    return 'argon2' if RE_ARGON2.match?(s)
    return 'hex'    if RE_HEX.match?(s)
    return 'base64' if RE_BASE64.match?(s)

    'unknown'
  end

  # Identify candidate hash types for an input string.
  #
  # Always returns a HashInfo; never raises. An empty, unrecognised, or
  # wrong-length input simply yields an empty `candidates` list — the caller
  # decides whether "no candidates" means "not a hash".
  def identify_hash(input)
    original = input.to_s
    cleaned = original.strip
    charset = detect_charset(cleaned)
    length = cleaned.length
    candidates = []

    case charset
    when 'bcrypt'
      # bcrypt's modular-crypt token encodes a 184-bit effective hash.
      candidates << HashMatch.new(name: 'bcrypt', bit_length: 184)
    when 'argon2'
      # Argon2 output length is parameter-driven, so no fixed bit length applies.
      candidates << HashMatch.new(name: 'Argon2', bit_length: 0)
    when 'hex'
      # length*4 converts hex-char count to a bit width (4 bits per nibble).
      HEX_BY_LENGTH.fetch(length, []).each do |name|
        candidates << HashMatch.new(name: name, bit_length: length * 4)
      end
    when 'base64'
      # Each base64 char carries 6 bits; round to the nearest byte boundary
      # so the reported length lines up with the underlying digest width. All
      # table lengths divide evenly, so rounding is exact here.
      bit_length = ((length * 6) / 8.0).round * 8
      BASE64_BY_LENGTH.fetch(length, []).each do |name|
        candidates << HashMatch.new(name: name, bit_length: bit_length)
      end
    end

    HashInfo.new(input: original, cleaned: cleaned, length: length,
                 charset: charset, candidates: candidates)
  end
end

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 →