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 →