Skip to content

Latest commit

ย 

History

History
353 lines (287 loc) ยท 26.2 KB

File metadata and controls

353 lines (287 loc) ยท 26.2 KB

Architecture

This page is about how the code is shaped. Not what each line does โ€” that's the next page. Here we zoom out and look at how the pieces fit together, what data flows where, and why we picked this shape over the alternatives.

1. The big picture

The whole tool is one Python file: hash_identifier.py. Everything that runs lives in that file. There are three layers inside it:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  CLI layer  (main, _build_argument_parser, _render_table)   โ”‚
โ”‚  - reads command-line arguments                             โ”‚
โ”‚  - prints the colored table to your terminal                โ”‚
โ”‚  - returns an exit code                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚ calls
                           โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Pure-function layer  (identify)                            โ”‚
โ”‚  - the actual decision-making                               โ”‚
โ”‚  - takes a string, returns a list of HashCandidate          โ”‚
โ”‚  - touches NO files, NO network, NO global state            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ”‚ uses
                           โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Data layer  (PREFIX_RULES, HEX_LENGTH_RULES, charsets)     โ”‚
โ”‚  - lookup tables describing what we know about hashes       โ”‚
โ”‚  - read-only, defined at module load time                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Reading top to bottom, the CLI layer is the outside of the program: it deals with the human. The pure-function layer is the brain: it takes a clean string in and returns a clean answer out. The data layer is the knowledge: every "we know X about hash Y" lives in one of those tables, not scattered through the code.

This three-way split is deliberate. We can test the brain in isolation without ever spawning a CLI โ€” and the test file does exactly that, calling identify() directly. We can change how the table looks (color, layout, JSON output) without touching the brain. And we can add new hash formats by adding a row to a table, not by adding a new function.

2. Data flow on a single run

Here's what happens when you type just run -- 5f4dcc3b5aa765d61d8327deb882cf99:

                                            (your terminal)
                                                  โ”‚
                                                  โ”‚  "5f4dcc3b5aa765d61d8327deb882cf99"
                                                  โ–ผ
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚  argparse                           โ”‚
                              โ”‚  parses sys.argv into args.hash     โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ”‚ args.hash = "5f4dcc..."
                                               โ–ผ
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚  identify(args.hash)                โ”‚
                              โ”‚                                     โ”‚
                              โ”‚  text = args.hash.strip()           โ”‚
                              โ”‚                                     โ”‚
                              โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
                              โ”‚  โ”‚ Step 1: prefix match?       โ”‚    โ”‚
                              โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
                              โ”‚  no match     โ”‚                     โ”‚
                              โ”‚               โ–ผ                     โ”‚
                              โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
                              โ”‚  โ”‚ Step 2: special shape?      โ”‚    โ”‚
                              โ”‚  โ”‚  (NetNTLM / MySQL5 / DES)   โ”‚    โ”‚
                              โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
                              โ”‚  no match     โ”‚                     โ”‚
                              โ”‚               โ–ผ                     โ”‚
                              โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
                              โ”‚  โ”‚ Step 3: hex + length match? โ”‚    โ”‚
                              โ”‚  โ”‚  โ†’ 32 hex chars โ†’ MD5/NTLM..โ”‚ โœ”  โ”‚
                              โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
                              โ”‚               โ”‚                     โ”‚
                              โ”‚       returns [HashCandidate, ...]  โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ”‚
                                               โ–ผ
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚  _render_table()                    โ”‚
                              โ”‚  builds a rich.Table, prints it     โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ”‚
                                               โ–ผ
                                       (your terminal)

      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ”‚  Candidates for: 5f4dcc3b5aa765d61d8327deb882cf99       โ”‚
      โ”‚  โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚
      โ”‚  โ”‚ algorithm โ”‚ confidence โ”‚ reason                    โ”‚ โ”‚
      โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
      โ”‚  โ”‚ MD5       โ”‚ medium     โ”‚ 32 hex chars โ€” most likelyโ”‚ โ”‚
      โ”‚  โ”‚ NTLM      โ”‚ low        โ”‚ 32 hex chars โ€” also poss. โ”‚ โ”‚
      โ”‚  โ”‚ MD4       โ”‚ low        โ”‚ 32 hex chars โ€” also poss. โ”‚ โ”‚
      โ”‚  โ”‚ RIPEMD-128โ”‚ low        โ”‚ 32 hex chars โ€” also poss. โ”‚ โ”‚
      โ”‚  โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The brain is the middle box. Everything above and below it is just plumbing: getting the string in, getting the table out.

3. The six-step decision pipeline

The brain (identify()) is structured as six numbered steps. Each step is a chance to short-circuit and return a verdict. If a step matches, the function returns immediately. If not, control falls through to the next step.

This shape โ€” "try the strongest signal first, fall back to weaker ones" โ€” is called a decision cascade or rule pipeline. You'll see this pattern all over security tooling: spam filters, IDS rules, antivirus heuristics, fingerprinting. They all share the same skeleton.

        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 1: PREFIX_RULES?          โ”‚ HIGH confidence
        โ”‚ Walk the prefix table.         โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  return
        โ”‚ Any prefix start match wins.   โ”‚ first match
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚ no match
                      โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 2: special shapes?        โ”‚ HIGH/MEDIUM
        โ”‚ NetNTLMv2 / NetNTLMv1 (`::`)   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  return
        โ”‚ MySQL5  (`*` + 40 upper hex)   โ”‚ first match
        โ”‚ DES crypt  (13 chars)          โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚ no match
                      โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 3: pure hex?              โ”‚ MEDIUM/LOW
        โ”‚ If yes, look up length in      โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  return
        โ”‚ HEX_LENGTH_RULES, return all   โ”‚ ranked list
        โ”‚ candidates ranked by likelihoodโ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚ not hex
                      โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 4: generic `$algo$...`?   โ”‚ LOW
        โ”‚ Looks like a PHC string but    โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  return
        โ”‚ we have no specific rule.      โ”‚ generic match
        โ”‚ Report it as a generic PHC.    โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚ no
                      โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 5: shape hint?            โ”‚ LOW
        โ”‚ Looks like a JWT (eyJ...) or   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  return
        โ”‚ base64 (`+`, `/`, `=`)?        โ”‚ "not a hash"
        โ”‚ Tell the user it's not a hash. โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚ no
                      โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Step 6: give up.               โ”‚ none
        โ”‚ Return empty list.             โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ  []
        โ”‚ CLI prints "could not identify"โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Order matters here, and the order isn't arbitrary. We always try the most specific test first and the most general test last:

  1. PHC prefixes are dead giveaways โ€” the hash itself names the algorithm.
  2. Special shapes (NetNTLM, MySQL5, DES crypt) are also strong: they have distinctive structures.
  3. Hex + length is a narrowing signal, not a definitive one โ€” it picks a family, not a member.
  4. Generic PHC fallback catches hashes that look PHC-shaped but aren't in our table.
  5. Shape hints handle the common "I pasted the wrong thing" case (people drop JWTs into hash identifiers all the time).
  6. Empty list = honest "I don't know."

If we reversed the order โ€” say, checked length before prefix โ€” we would misclassify a bcrypt hash as "60 hex chars, no length rule" because we'd never look at its $2b$ prefix. Order encodes priority.

4. The HashCandidate object

The brain doesn't return a string โ€” it returns a list of HashCandidate objects. A candidate has three fields:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  HashCandidate                                              โ”‚
โ”‚                                                             โ”‚
โ”‚    algorithm:   str         e.g. "MD5", "bcrypt", "SHA-256" โ”‚
โ”‚    confidence:  Literal     "high" | "medium" | "low"       โ”‚
โ”‚    reason:      str         "prefix `$2b$` โ€” bcrypt PHC..." โ”‚
โ”‚                                                             โ”‚
โ”‚  frozen=True   โ”€โ”€ immutable, can't be mutated after creationโ”‚
โ”‚  slots=True    โ”€โ”€ memory-efficient (no __dict__)            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The shape is intentional. algorithm is what hashcat wants to know. confidence tells the human reader how much to trust this guess. reason is the evidence โ€” a one-line explanation of why the tool made this guess. The reason field is what makes the tool teachable: the user sees not just "bcrypt" but "prefix $2b$ โ€” bcrypt PHC string, 2b variant (current)."

We use @dataclass(frozen=True, slots=True) instead of writing a class with __init__ and __repr__ by hand:

  • frozen=True means once you build a HashCandidate, you can't mutate it. If somewhere in the code tried candidate.algorithm = "something else", Python would raise FrozenInstanceError. This makes the data flow predictable: a candidate that comes out of identify() is the same candidate everywhere it shows up later.
  • slots=True is a memory optimization. Without slots, every instance carries around a __dict__ for adding attributes on the fly. We don't need that, so we turn it off and save memory.

Both flags also signal intent to a reader: "this is a value object, not a mutable bag of state." That signal matters more than the bytes saved.

5. The data tables as the source of truth

If you wanted to add a new hash format to this tool, you would not write new logic. You would add a row to one of these tables:

PREFIX_RULES: list of (prefix, algorithm, note)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
("$argon2id$", "Argon2id", "modern PHC string..."),
("$2b$",       "bcrypt",   "bcrypt PHC string..."),
("$6$",        "SHA-512 crypt", "Unix crypt..."),
... ~25 more rows


HEX_LENGTH_RULES: dict of {length_in_hex_chars: [algorithms]}
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32:  ["MD5", "NTLM", "MD4", "RIPEMD-128"],
40:  ["SHA-1", "RIPEMD-160"],
64:  ["SHA-256", "SHA3-256", "BLAKE2s-256", "RIPEMD-256"],
128: ["SHA-512", "SHA3-512", "BLAKE2b-512", "Whirlpool"],
... etc


HEX_CHARSET, _HEX_UPPER_CHARSET, _DESCRYPT_CHARSET
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
The alphabets used by each format. frozenset for fast lookup.

This is called a data-driven design. The rules live in data, not code. Three benefits:

  1. Adding a new format is one line. No new function, no new test scaffolding to write.
  2. The rules are inspectable. You can read PREFIX_RULES and immediately see every format the tool knows.
  3. The rules are testable. The test file iterates over PREFIX_RULES and confirms each prefix is recognized โ€” so the data and the behavior cannot drift out of sync.

When you read the implementation page next, watch how few of the function bodies have if/elif/elif chains. The decisions happen inside table lookups, not inside conditionals. That's the data-driven design at work.

6. Two helper functions

The brain is one big function (identify), but two small helpers live next to it. They both answer yes/no questions about the input string:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  _is_hex(text) -> bool                                   โ”‚
โ”‚    "Is every character of text a valid hex digit?"       โ”‚
โ”‚    Used in step 3 to decide whether to look up length.   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  _is_mysql5(text) -> bool                                โ”‚
โ”‚    "Does text look like `*` + 40 uppercase hex chars?"   โ”‚
โ”‚    Used in step 2 for MySQL5 detection.                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  _is_descrypt(text) -> bool                              โ”‚
โ”‚    "Is text 13 chars from `./0-9A-Za-z`?"                โ”‚
โ”‚    Used in step 2 for legacy DES crypt detection.        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The leading underscore (_is_hex, not is_hex) is a Python convention meaning "this is module-private." It says to other developers: "this is an implementation detail of hash_identifier.py; don't import it from somewhere else." Python doesn't enforce this โ€” you can still import private names โ€” but every linter and every reviewer will flag you if you do.

The helpers are tiny on purpose. Each one is a single boolean question. We pull them out of identify() not because they're complicated but because giving them a name makes identify() read like English: "if _is_hex(text), do hex-length matching." If we inlined the test, the eye would have to parse it.

7. The CLI layer

The CLI layer is the part the human actually interacts with. It does three things:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ _build_argument_parser()                                   โ”‚
โ”‚   Sets up argparse โ€” defines that the program takes one    โ”‚
โ”‚   positional argument (`hash`) and an optional `--top N`   โ”‚
โ”‚   flag. Returns a configured parser.                       โ”‚
โ”‚                                                            โ”‚
โ”‚   Pulled out of main() so tests can build the parser       โ”‚
โ”‚   without actually running the CLI.                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ _render_table(raw_input, candidates, console)              โ”‚
โ”‚   Builds a rich.Table object, adds one row per candidate,  โ”‚
โ”‚   colors the confidence column (green/yellow/cyan),        โ”‚
โ”‚   and prints it.                                           โ”‚
โ”‚                                                            โ”‚
โ”‚   Takes the rich Console as an argument so tests can pass  โ”‚
โ”‚   a captured-output Console and verify what got printed.   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ main()                                                     โ”‚
โ”‚   Parses args, calls identify(), prints the table.         โ”‚
โ”‚   Returns an exit code:                                    โ”‚
โ”‚     0 โ†’ at least one candidate found                       โ”‚
โ”‚     1 โ†’ no candidates (printed an error message)           โ”‚
โ”‚                                                            โ”‚
โ”‚   The exit code lets shell scripts do                      โ”‚
โ”‚       `if hashid "$x"; then ...`                           โ”‚
โ”‚   to react to whether identification succeeded.            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The crucial design choice in the CLI layer is dependency injection. _render_table takes a Console object as a parameter instead of creating one inside. That sounds fancy, but it just means: "give me the printer to use." The function doesn't care if you give it a real terminal Console or a test Console that captures output to a string. This makes the function testable without writing to your real terminal during tests.

8. The test file mirrors the brain

test_hash_identifier.py is structured to mirror hash_identifier.py. It tests every behavior the tool claims to have:

test_bcrypt_prefix_is_recognized            โ”€โ”€ covers PREFIX_RULES row $2b$
test_argon2id_prefix_is_recognized          โ”€โ”€ covers PREFIX_RULES row $argon2id$
test_apr1_prefix_is_recognized              โ”€โ”€ covers PREFIX_RULES row $apr1$
test_sha512_crypt_prefix_is_recognized      โ”€โ”€ covers PREFIX_RULES row $6$
test_django_pbkdf2_prefix_is_recognized     โ”€โ”€ covers PREFIX_RULES row pbkdf2_sha256$

test_mysql5_format_is_recognized            โ”€โ”€ covers Step 2 / _is_mysql5
test_mysql5_rejects_lowercase_body          โ”€โ”€ covers the "be honest, don't lie" rule
test_netntlmv2_format_is_recognized         โ”€โ”€ covers Step 2 / NetNTLMv2
test_netntlmv1_format_is_recognized         โ”€โ”€ covers Step 2 / NetNTLMv1
test_descrypt_format_is_recognized          โ”€โ”€ covers Step 2 / _is_descrypt

test_md5_length_returns_md5_first           โ”€โ”€ covers Step 3 / 32 hex chars
test_sha1_length_returns_sha1_first         โ”€โ”€ covers Step 3 / 40 hex chars
test_sha256_length_returns_sha256_first     โ”€โ”€ covers Step 3 / 64 hex chars
test_mysql323_length_returns_mysql323_first โ”€โ”€ covers Step 3 / 16 hex chars

test_unknown_phc_string_falls_back_to_generic
                                            โ”€โ”€ covers Step 4

test_jwt_input_is_called_out_as_not_a_hash  โ”€โ”€ covers Step 5
test_base64_blob_is_called_out_as_not_a_hashโ”€โ”€ covers Step 5

test_empty_input_returns_no_candidates      โ”€โ”€ covers Step 6 (edge case)
test_garbage_returns_no_candidates          โ”€โ”€ covers Step 6
test_input_is_trimmed_of_whitespace         โ”€โ”€ covers the .strip() at the top

test_hash_candidate_is_frozen               โ”€โ”€ covers the @dataclass(frozen=True)

test_every_prefix_rule_is_recognized_with_high_confidence
                                            โ”€โ”€ meta-test: iterates over
                                            โ”€โ”€ PREFIX_RULES and asserts every
                                            โ”€โ”€ row produces a HIGH-confidence
                                            โ”€โ”€ match. Keeps data and code in sync.

The meta-test at the bottom is the most interesting one. It's a guard against future regressions: if you add a new row to PREFIX_RULES and forget to update the matching logic, this test fires. The test loops over the data, not over hardcoded inputs โ€” so the test grows automatically as the data table grows.

9. Why pure functions matter here

The brain (identify()) is what's called a pure function:

  • Given the same input, it always returns the same output.
  • It doesn't modify anything outside itself (no global variables, no files, no network).
  • It doesn't depend on anything outside itself (no current time, no environment variables, no random numbers).

This sounds like a small thing. It's enormous. Pure functions are:

  • Trivially testable. assert identify("5f4d...") == [HashCandidate(...)]. No mocking, no setup, no teardown.
  • Trivially parallelizable. You could run identify() on a million hashes across 16 CPU cores with zero coordination, because no two calls can interfere with each other.
  • Trivially cacheable. Same input โ†’ same output โ†’ memoize freely.
  • Trivially understandable. You can read identify() in isolation. You don't have to know what state the program is in.

Most real programs can't be all-pure โ€” they have to read files, send packets, write to databases. But you can almost always carve out a pure core and put a thin shell around it that does the side-effecty stuff. That's exactly the architecture here: pure brain in the middle, side-effecty CLI shell around it.

This is sometimes called the Functional Core, Imperative Shell pattern. It's worth learning the name because once you see it, you'll spot it everywhere.

10. Next up

You now know the shape: three layers, six steps, three data tables, three helpers, one HashCandidate record, one CLI shell. Read 03-IMPLEMENTATION.md next and we'll walk every line of hash_identifier.py together.