Skip to content

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

// Hash-type identifier — Swift port.
//
// Language: Swift (5.9+, standard library only — no Foundation needed)
// 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 traps.

/// The character set classification of a candidate hash string.
///
/// The raw value is the lowercase identifier matching the TypeScript string
/// literal used by the canonical implementation (so serialised output agrees).
public enum HashCharset: String, Sendable {
    case hex
    case base64
    case bcrypt
    case argon2
    case unknown
}

/// A candidate hash algorithm and its nominal bit length
/// (`bitLength` = hex length * 4, where applicable).
public struct HashMatch: Equatable, Sendable {
    public let name: String
    public let bitLength: Int

    public init(name: String, bitLength: Int) {
        self.name = name
        self.bitLength = bitLength
    }
}

/// The full identification result for an input string.
public struct HashInfo: Equatable, Sendable {
    public let input: String
    public let cleaned: String  // trimmed input
    public let length: Int
    public let charset: HashCharset
    public let candidates: [HashMatch]

    public init(input: String, cleaned: String, length: Int,
                charset: HashCharset, candidates: [HashMatch]) {
        self.input = input
        self.cleaned = cleaned
        self.length = length
        self.charset = charset
        self.candidates = candidates
    }
}

// MARK: - Candidate tables

/// Hex candidates for a given hex-string length, if any.
///
/// Each hex char encodes 4 bits, so a 64-char digest implies a 256-bit
/// algorithm such as SHA-256.
func hexCandidates(forLength length: Int) -> [String]? {
    switch length {
    case 8:   return ["CRC32", "Adler-32"]
    case 16:  return ["MySQL 3.x", "CRC64"]
    case 32:  return ["MD5", "MD4", "NTLM", "LM", "MD2", "RIPEMD-128", "HAVAL-128"]
    case 40:  return ["SHA-1", "RIPEMD-160", "HAVAL-160", "MySQL 5.x (SHA1(SHA1))", "Tiger-160"]
    case 56:  return ["SHA-224", "SHA3-224", "BLAKE2s-224", "HAVAL-224"]
    case 64:  return ["SHA-256", "SHA3-256", "BLAKE2s-256", "RIPEMD-256", "Skein-256"]
    case 96:  return ["SHA-384", "SHA3-384", "BLAKE2b-384"]
    case 128: return ["SHA-512", "SHA3-512", "BLAKE2b-512", "Whirlpool", "Skein-512"]
    default:  return nil
    }
}

/// Base64 candidates for a given encoded-string length, if any
/// (16-byte MD5 digest -> 24 base64 chars including padding, etc.).
func base64Candidates(forLength length: Int) -> [String]? {
    switch length {
    case 24: return ["MD5 (base64)"]
    case 28: return ["SHA-1 (base64)"]
    case 44: return ["SHA-256 (base64)"]
    case 88: return ["SHA-512 (base64)"]
    default: return nil
    }
}

// MARK: - Detection

/// Matches the bcrypt modular-crypt prefix `^\$2[abxy]?\$` — prefix match
/// only; the variable trailing payload is not inspected.
func looksLikeBcrypt(_ s: Substring) -> Bool {
    guard s.hasPrefix("$2") else { return false }
    let chars = Array(s)  // hash tokens are short ASCII — index by Character
    guard chars.count >= 3 else { return false }
    switch chars[2] {
    case "a", "b", "x", "y":
        return chars.count >= 4 && chars[3] == "$"
    case "$":
        return true
    default:
        return false
    }
}

/// Matches the argon2 modular-crypt prefix `^\$argon2(id|i|d)?\$`.
func looksLikeArgon2(_ s: Substring) -> Bool {
    guard s.hasPrefix("$argon2") else { return false }
    let rest = Array(s.dropFirst("$argon2".count))
    // Try the two-char variant first so `id` wins over the bare `i`.
    if rest.count >= 2, rest[0] == "i", rest[1] == "d" {
        return rest.count >= 3 && rest[2] == "$"
    }
    guard let first = rest.first else { return false }
    switch first {
    case "i", "d":
        return rest.count >= 2 && rest[1] == "$"
    case "$":
        return true
    default:
        return false
    }
}

/// Whole-string hex match, mirroring the `+` quantifier (non-empty body of
/// ASCII hex digits).
func looksLikeHex(_ s: Substring) -> Bool {
    !s.isEmpty && s.unicodeScalars.allSatisfy { $0.properties.isASCIIHexDigit }
}

/// Valid standard-alphabet base64 with 0–2 trailing `=` padding; the body
/// before padding must be non-empty.
func looksLikeBase64(_ s: Substring) -> Bool {
    // Strip up to two trailing `=` padding characters, then require the
    // remaining body to be non-empty and entirely base64 alphabet scalars.
    var scalars = Array(s.unicodeScalars)
    var pad = 0
    while let last = scalars.last, last == "=", pad < 2 {
        scalars.removeLast()
        pad += 1
    }
    return !scalars.isEmpty && scalars.allSatisfy(isBase64Scalar)
}

private func isBase64Scalar(_ scalar: Unicode.Scalar) -> Bool {
    scalar.properties.isASCIIHexDigit
        || ("A"..."Z").contains(scalar)
        || scalar == "+"
        || scalar == "/"
}

/// Classify the charset of a candidate hash string.
///
/// 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 func detectCharset(_ s: String) -> HashCharset {
    let sub = Substring(s)
    if looksLikeBcrypt(sub) { return .bcrypt }
    if looksLikeArgon2(sub) { return .argon2 }
    if looksLikeHex(sub) { return .hex }
    if looksLikeBase64(sub) { return .base64 }
    return .unknown
}

/// The six ASCII whitespace bytes removed by the canonical `trim()`.
/// Written by hand so the port needs no Foundation.
private let asciiWhitespace: Set<Unicode.Scalar> = [
    " ", "\t", "\n", "\u{0B}", "\u{0C}", "\r",
]

private func asciiTrim(_ input: String) -> Substring {
    var scalars = input.unicodeScalars
    while let first = scalars.first, asciiWhitespace.contains(first) {
        scalars.removeFirst()
    }
    while let last = scalars.last, asciiWhitespace.contains(last) {
        scalars.removeLast()
    }
    return Substring(String(scalars))
}

/// Identify candidate hash types for an input string.
///
/// Always returns a fully populated `HashInfo`; never traps. A nil 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 func identifyHash(_ input: String?) -> HashInfo {
    let original = input ?? ""
    let cleaned = asciiTrim(original)
    let charset = detectCharset(String(cleaned))
    let length = cleaned.count

    var candidates: [HashMatch] = []
    switch charset {
    case .bcrypt:
        // bcrypt's modular-crypt token encodes a 184-bit effective hash.
        candidates.append(HashMatch(name: "bcrypt", bitLength: 184))
    case .argon2:
        // Argon2 output length is parameter-driven, so no fixed bit length applies.
        candidates.append(HashMatch(name: "Argon2", bitLength: 0))
    case .hex:
        // length*4 converts hex-char count to a bit width (4 bits per nibble).
        if let names = hexCandidates(forLength: length) {
            candidates = names.map { HashMatch(name: $0, bitLength: length * 4) }
        }
    case .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.
        if let names = base64Candidates(forLength: length) {
            let bits = Int((Double(length) * 6 / 8).rounded()) * 8
            candidates = names.map { HashMatch(name: $0, bitLength: bits) }
        }
    case .unknown:
        break
    }

    return HashInfo(input: original, cleaned: String(cleaned), length: length,
                    charset: charset, candidates: 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 →