feat: complete project hash-cracker
This commit is contained in:
parent
e76a7224ba
commit
9e314908b2
|
|
@ -4,3 +4,5 @@
|
|||
*.coverage
|
||||
.env
|
||||
.angelusvigil/
|
||||
|
||||
PROJECTS/beginner/hash-cracker/docs/
|
||||
|
|
|
|||
|
|
@ -26,11 +26,8 @@ find_package(OpenSSL REQUIRED)
|
|||
find_package(Boost REQUIRED COMPONENTS program_options)
|
||||
|
||||
set(HASHCRACKER_SOURCES
|
||||
src/hash/MD5Hasher.cpp
|
||||
src/hash/SHA1Hasher.cpp
|
||||
src/hash/SHA256Hasher.cpp
|
||||
src/hash/SHA512Hasher.cpp
|
||||
src/hash/HashDetector.cpp
|
||||
src/io/MappedFile.cpp
|
||||
src/attack/DictionaryAttack.cpp
|
||||
src/attack/BruteForceAttack.cpp
|
||||
src/attack/RuleAttack.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
# Hash Cracker CLI Tool
|
||||
|
||||
## What This Is
|
||||
|
||||
A multi-threaded command line tool that recovers plaintext passwords from cryptographic hashes. It supports MD5, SHA1, SHA256, and SHA512 using three attack strategies: dictionary attacks from wordlists, brute force generation of all possible combinations, and rule-based mutations that transform dictionary words into common password variations like `P@ssw0rd123`.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
When attackers breach a database, they don't get plaintext passwords. They get hashes. The security of every user in that database depends on how resistant those hashes are to cracking. Building a cracker teaches you exactly why unsalted fast hashes like MD5 are catastrophic for password storage, and why modern systems use bcrypt or argon2 instead.
|
||||
|
||||
**Real world scenarios where this applies:**
|
||||
- Penetration testers use tools like hashcat and john the ripper to audit password strength after gaining access to hash dumps
|
||||
- The 2012 LinkedIn breach leaked 6.5 million unsalted SHA1 hashes. Researchers cracked 90% within 72 hours
|
||||
- The 2013 Adobe breach exposed 153 million accounts using 3DES encryption (not even hashing) with no unique salts. The same encrypted blob appeared millions of times because identical passwords produced identical ciphertext
|
||||
|
||||
## What You'll Learn
|
||||
|
||||
**Security Concepts:**
|
||||
- Cryptographic hash functions: one-way transformations that can't be reversed
|
||||
- Dictionary attacks: leveraging known password lists from real breaches
|
||||
- Brute force attacks: exhaustive search through all possible character combinations
|
||||
- Rule-based mutations: why `Password123!` is not a strong password
|
||||
- Salting: what it prevents (rainbow tables) and what it doesn't prevent (targeted cracking)
|
||||
|
||||
**Technical Skills:**
|
||||
- Policy-based template design in C++23 with concept constraints
|
||||
- `std::expected` for composable error handling without exceptions
|
||||
- `std::generator` for lazy evaluation with coroutines
|
||||
- Memory-mapped file I/O with mmap for zero-copy wordlist reading
|
||||
- Multi-threaded work partitioning with `std::jthread` and atomics
|
||||
- OpenSSL EVP API for hash computation
|
||||
|
||||
**Tools and Techniques:**
|
||||
- CMake with presets for reproducible builds
|
||||
- GoogleTest for unit and integration testing
|
||||
- OpenSSL for cryptographic hash functions
|
||||
- Boost.program_options for CLI argument parsing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Required knowledge:**
|
||||
- C++ basics: classes, templates, lambdas, move semantics
|
||||
- Understanding of what a hash function does (input goes in, fixed-length output comes out, you can't reverse it)
|
||||
- Command line familiarity
|
||||
|
||||
**Tools you'll need:**
|
||||
- GCC 14 or higher (C++23 support required)
|
||||
- CMake 3.25+
|
||||
- Ninja build system
|
||||
- OpenSSL development headers
|
||||
- Boost.program_options
|
||||
|
||||
**Helpful but not required:**
|
||||
- Experience with templates and concepts
|
||||
- Familiarity with threading and atomics
|
||||
- Understanding of memory-mapped I/O
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd PROJECTS/beginner/hash-cracker
|
||||
|
||||
./install.sh
|
||||
|
||||
hashcracker --hash 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 \
|
||||
--wordlist wordlists/10k-most-common.txt
|
||||
```
|
||||
|
||||
Expected output: The tool auto-detects the hash as SHA256, searches the 10,000-word dictionary, and finds the password `password` in under a second. If your terminal supports it, you'll see a progress bar with speed and ETA while it runs.
|
||||
|
||||
Try brute force:
|
||||
```bash
|
||||
hashcracker --hash 187ef4436122d1cc2f40dc2b92f0eba0 \
|
||||
--bruteforce --charset lower --max-length 4 --type md5
|
||||
```
|
||||
|
||||
Expected output: Generates all lowercase combinations up to 4 characters, finds `ab` after searching ~350,000 candidates.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
hash-cracker/
|
||||
├── main.cpp CLI parsing and dispatch
|
||||
├── src/
|
||||
│ ├── config/Config.hpp Constants, character sets, colors
|
||||
│ ├── core/
|
||||
│ │ ├── Concepts.hpp Hasher and AttackStrategy concepts
|
||||
│ │ └── Engine.hpp Template engine (hasher + attack + threading)
|
||||
│ ├── hash/
|
||||
│ │ ├── EVPHasher.hpp Unified OpenSSL EVP hasher template
|
||||
│ │ ├── HashDetector.hpp Auto-detect hash type from hex length
|
||||
│ │ ├── MD5Hasher.hpp Type alias for EVPHasher<EVP_md5, ...>
|
||||
│ │ ├── SHA1Hasher.hpp Type alias for EVPHasher<EVP_sha1, ...>
|
||||
│ │ ├── SHA256Hasher.hpp Type alias for EVPHasher<EVP_sha256, ...>
|
||||
│ │ └── SHA512Hasher.hpp Type alias for EVPHasher<EVP_sha512, ...>
|
||||
│ ├── attack/
|
||||
│ │ ├── DictionaryAttack mmap wordlist reader with partitioning
|
||||
│ │ ├── BruteForceAttack Keyspace generator with partitioning
|
||||
│ │ └── RuleAttack Dictionary + mutation rules
|
||||
│ ├── rules/RuleSet Mutation transforms via std::generator
|
||||
│ ├── io/MappedFile RAII wrapper for mmap
|
||||
│ ├── threading/ThreadPool std::jthread work partitioning
|
||||
│ └── display/Progress Terminal progress display
|
||||
├── tests/ GoogleTest suite (38 tests)
|
||||
├── wordlists/ Included 10k common passwords
|
||||
├── install.sh One-command setup
|
||||
├── Justfile Build/test/clean commands
|
||||
└── CMakeLists.txt Build configuration
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Understand the concepts** - Read [01-CONCEPTS.md](./01-CONCEPTS.md) to learn how hash cracking works and why it matters
|
||||
2. **Study the architecture** - Read [02-ARCHITECTURE.md](./02-ARCHITECTURE.md) to see how concepts, templates, and threading fit together
|
||||
3. **Walk through the code** - Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for a detailed code walkthrough
|
||||
4. **Extend the project** - Read [04-CHALLENGES.md](./04-CHALLENGES.md) to add GPU acceleration, rainbow tables, or new algorithms
|
||||
|
||||
## Common Issues
|
||||
|
||||
**"File not found" when using a wordlist**
|
||||
```
|
||||
Error: File not found
|
||||
```
|
||||
Solution: Paths are relative to where you run the command. Run from the project root directory, or use an absolute path.
|
||||
|
||||
**"Invalid hash format" error**
|
||||
```
|
||||
Error: Invalid hash format
|
||||
```
|
||||
Solution: The auto-detector validates that all characters are hexadecimal (0-9, a-f) and the length matches a known algorithm (32=MD5, 40=SHA1, 64=SHA256, 128=SHA512). Check for trailing whitespace or non-hex characters.
|
||||
|
||||
**Brute force is slow on long passwords**
|
||||
This is expected. The keyspace grows exponentially. 6 lowercase characters = 308 million combinations. Add digits and it's 2.2 billion. Add uppercase and special characters and you're looking at hours or days. This is the whole point: strong passwords resist brute force.
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
# Core Security Concepts
|
||||
|
||||
This document explains the security concepts behind hash cracking. These aren't just definitions. We'll dig into how attacks actually work, why certain defenses exist, and what real breaches teach us about password storage.
|
||||
|
||||
## Cryptographic Hash Functions
|
||||
|
||||
### What They Are
|
||||
|
||||
A hash function takes input of any length and produces a fixed-length output (the digest). The same input always produces the same output, but you can't work backwards from the output to recover the input. This is a one-way transformation, not encryption.
|
||||
|
||||
```
|
||||
"password" → SHA256 → 5e884898da28047151d0e56f8dc... (always this, every time)
|
||||
"password1" → SHA256 → 0b14d501a594442a01c6859541bc... (completely different)
|
||||
"Password" → SHA256 → 22ee405817c14cbf9d3c2b92b87c... (completely different again)
|
||||
```
|
||||
|
||||
Three properties make hash functions useful for password storage:
|
||||
|
||||
1. **Deterministic**: Same input, same output. The server can verify your password by hashing what you type and comparing to the stored hash
|
||||
2. **Pre-image resistant**: Given a hash, you can't compute the input that produced it. Even if an attacker steals the hash database, they don't have plaintext passwords
|
||||
3. **Avalanche effect**: Changing one bit of input changes roughly half the output bits. "password" and "Password" produce completely unrelated hashes
|
||||
|
||||
### Algorithms This Tool Supports
|
||||
|
||||
| Algorithm | Digest Length | Status | Real World Usage |
|
||||
|-----------|--------------|--------|-----------------|
|
||||
| MD5 | 128 bits (32 hex chars) | Broken since 2004 | Still found in legacy systems, WordPress before 4.x |
|
||||
| SHA1 | 160 bits (40 hex chars) | Broken since 2017 | LinkedIn breach (2012), Git (transitioning away) |
|
||||
| SHA256 | 256 bits (64 hex chars) | Secure | Bitcoin, TLS certificates, many web frameworks |
|
||||
| SHA512 | 512 bits (128 hex chars) | Secure | Linux /etc/shadow default, some enterprise systems |
|
||||
|
||||
"Broken" for MD5 and SHA1 means researchers can generate collisions (two different inputs producing the same hash). For password cracking, the bigger problem is speed: these were designed to be fast, which is exactly what you don't want for password hashing.
|
||||
|
||||
### What Hash Functions Are NOT
|
||||
|
||||
Hash functions are not encryption. Encryption is reversible with a key. Hashing is not reversible at all. You don't "decrypt" a hash. You guess inputs, hash them, and check if the output matches. That's cracking.
|
||||
|
||||
## Dictionary Attacks
|
||||
|
||||
### How They Work
|
||||
|
||||
A dictionary attack tries every word from a list of known passwords. The attacker hashes each word and compares it to the target hash:
|
||||
|
||||
```
|
||||
Target: 5e884898da28047151d0e56f8dc6292773603d0d...
|
||||
|
||||
Hash("123456") → "e10adc..." ≠ target
|
||||
Hash("password") → "5e8848..." = target ← found it
|
||||
```
|
||||
|
||||
This works because people choose predictable passwords. The RockYou breach in 2009 leaked 32 million plaintext passwords, and the top 10 were:
|
||||
|
||||
```
|
||||
1. 123456 6. monkey
|
||||
2. 12345 7. 1234567
|
||||
3. 123456789 8. letmein
|
||||
4. password 9. trustno1
|
||||
5. iloveyou 10. dragon
|
||||
```
|
||||
|
||||
These lists get reused across every cracking tool. A 14 million word dictionary takes seconds to exhaust against a fast hash like SHA256 on modern hardware.
|
||||
|
||||
### Why They're So Effective
|
||||
|
||||
HaveIBeenPwned tracks over 613 million passwords from real breaches. If your password has ever appeared in any breach, it's in a cracking dictionary. Password reuse makes this worse: the password you use for a throwaway forum might end up in a dictionary used to crack your email account.
|
||||
|
||||
## Brute Force Attacks
|
||||
|
||||
### How They Work
|
||||
|
||||
Brute force generates every possible combination of characters up to a maximum length:
|
||||
|
||||
```
|
||||
Length 1: a, b, c, ..., z (26 combinations)
|
||||
Length 2: aa, ab, ac, ..., zz (676 combinations)
|
||||
Length 3: aaa, aab, ..., zzz (17,576 combinations)
|
||||
Length 4: aaaa, ..., zzzz (456,976 combinations)
|
||||
...
|
||||
Length 8: aaaaaaaa, ..., zzzzzzzz (208 billion combinations)
|
||||
```
|
||||
|
||||
The keyspace grows exponentially. Add uppercase letters and the base goes from 26 to 52. Add digits and it's 62. Add special characters and it's 95. An 8-character password using all character types has 95^8 = 6.6 quadrillion combinations.
|
||||
|
||||
### The Math of Feasibility
|
||||
|
||||
At 3 million SHA256 hashes per second (what this tool achieves on CPU):
|
||||
|
||||
| Password | Character Set | Combinations | Time |
|
||||
|----------|--------------|-------------|------|
|
||||
| 4 chars | lowercase | 456K | 0.15 seconds |
|
||||
| 6 chars | lowercase | 308M | 1.7 minutes |
|
||||
| 6 chars | lower + digits | 2.2B | 12 minutes |
|
||||
| 8 chars | lowercase | 208B | 19 hours |
|
||||
| 8 chars | all printable | 6.6Q | 70,000 years |
|
||||
|
||||
GPU cracking tools like hashcat achieve 3 billion per second (1000x faster), but even then 8 characters with full character sets takes 25 days. This is why password length matters more than complexity.
|
||||
|
||||
## Rule-Based Mutations
|
||||
|
||||
### How They Work
|
||||
|
||||
Humans are predictable. When forced to add a capital letter, most people capitalize the first letter. When forced to add a number, they append it. When forced to add a special character, they use `!` or `@`. Rule based attacks exploit these patterns:
|
||||
|
||||
| Rule | Input | Output |
|
||||
|------|-------|--------|
|
||||
| Capitalize first | password | Password |
|
||||
| Uppercase all | password | PASSWORD |
|
||||
| Leet substitution | password | p@$$w0rd |
|
||||
| Append digits 0-999 | password | password123 |
|
||||
| Reverse | password | drowssap |
|
||||
| Toggle case | password | PASSWORD |
|
||||
|
||||
This turns a 14 million word dictionary into billions of candidates. The combination of `capitalize + leet + append digits` transforms `password` into `P@$$w0rd123`, which satisfies every password policy ever written and still cracks in milliseconds.
|
||||
|
||||
### Why Complexity Requirements Don't Work
|
||||
|
||||
The 2021 Specops analysis of 800 million breached passwords found that 83% met standard complexity requirements (8+ characters, uppercase, lowercase, number, special character). The most common patterns:
|
||||
|
||||
```
|
||||
[Word][Number] → Password1
|
||||
[Word][Number][!] → Password1!
|
||||
[Season][Year] → Summer2024
|
||||
[Name][Birthday] → Michael1990!
|
||||
```
|
||||
|
||||
Password managers generating random strings like `x7$kQ2!mR9pL` are the actual defense. No dictionary contains that, and brute forcing 12 random characters from the full printable set is computationally infeasible.
|
||||
|
||||
## Password Salting
|
||||
|
||||
### What It Is
|
||||
|
||||
A salt is a random string stored alongside the password hash. Before hashing, the salt is prepended or appended to the password:
|
||||
|
||||
```
|
||||
Without salt:
|
||||
User A: SHA256("password") → 5e884898da...
|
||||
User B: SHA256("password") → 5e884898da... (identical!)
|
||||
|
||||
With salt:
|
||||
User A: SHA256("x9f2" + "password") → a1b2c3d4...
|
||||
User B: SHA256("k7m1" + "password") → 9z8y7x6w... (completely different)
|
||||
```
|
||||
|
||||
The salt is not secret. It's stored in plaintext right next to the hash in the database. Its only job is to make each user's hash unique, even if they use the same password.
|
||||
|
||||
### What Salt Prevents
|
||||
|
||||
**Rainbow tables** are precomputed lookup tables mapping hashes to passwords. Without salt, you compute SHA256("password") once and it matches every user who chose "password". A rainbow table for SHA256 covering common passwords might be 100GB, but it cracks millions of hashes instantly.
|
||||
|
||||
Salt makes rainbow tables useless. Each user has a different salt, so you'd need a separate rainbow table for every possible salt value. With a 16-byte salt, that's 2^128 possible tables. Not happening.
|
||||
|
||||
**Mass cracking** is also defeated. Without salt, if you crack one hash, you've cracked every user with that password. With salt, you crack one user at a time because each hash is computed differently.
|
||||
|
||||
### What Salt Does NOT Prevent
|
||||
|
||||
Salt does not slow down targeted cracking. If an attacker wants to crack one specific user's hash, they know the salt (it's in the database) and just prepend it to every guess. The cracking speed is identical. Our `--salt` flag demonstrates this exact attack.
|
||||
|
||||
### Real World Salt Failures
|
||||
|
||||
The 2012 LinkedIn breach leaked 6.5 million SHA1 hashes with no salt. Researchers cracked 90% within 72 hours. If LinkedIn had used salts, cracking would have required attacking each hash individually instead of cracking the entire database at once.
|
||||
|
||||
The 2013 Adobe breach used 3DES encryption (not even hashing) with a single key and no unique salts. Because identical passwords produced identical ciphertext, researchers could identify the most common passwords just by counting duplicates, then crack them based on password hints that were also leaked.
|
||||
|
||||
## Slow Hash Functions
|
||||
|
||||
### Why Fast Hashes Are the Problem
|
||||
|
||||
SHA256 was designed to hash data quickly. That's great for verifying file integrity or TLS handshakes, but terrible for password storage. A GPU can compute 3 billion SHA256 hashes per second. That means an attacker can try 3 billion passwords per second.
|
||||
|
||||
### How bcrypt and argon2 Fix This
|
||||
|
||||
bcrypt, scrypt, and argon2 are intentionally slow hash functions. They add a configurable "work factor" that controls how long each hash takes:
|
||||
|
||||
```
|
||||
SHA256: ~3,000,000 hashes/sec (CPU) ~3,000,000,000 hashes/sec (GPU)
|
||||
bcrypt: ~300 hashes/sec (CPU) ~50,000 hashes/sec (GPU)
|
||||
argon2: ~10 hashes/sec (CPU) GPU-resistant by design
|
||||
```
|
||||
|
||||
bcrypt at cost factor 12 takes about 250ms per hash. A user logging in waits 250ms (unnoticeable). An attacker trying 14 million dictionary words waits 40 days. Same math, completely different outcome.
|
||||
|
||||
argon2 goes further by requiring large amounts of memory per hash, which limits GPU parallelism. GPUs have thousands of cores but limited memory per core. argon2 exploits this by forcing each hash to use megabytes of RAM, making GPU cracking impractical.
|
||||
|
||||
### Why We Don't Crack Them
|
||||
|
||||
This tool only cracks fast hashes (MD5, SHA1, SHA256, SHA512). That's intentional. Cracking bcrypt with a 10,000-word dictionary at 300 hashes/sec takes 33 seconds. The same dictionary against SHA256 takes 3 milliseconds. The speed difference is the lesson.
|
||||
|
||||
## Industry Standards
|
||||
|
||||
**OWASP Password Storage Cheat Sheet** recommends:
|
||||
- argon2id as the primary choice
|
||||
- bcrypt with cost factor 10+ as the fallback
|
||||
- Never MD5 or SHA-family for passwords
|
||||
- Minimum 16-byte random salt per password
|
||||
|
||||
**NIST SP 800-63B** (Digital Identity Guidelines):
|
||||
- Memorized secrets should be hashed with a salt using a key derivation function
|
||||
- At least 10,000 iterations if using PBKDF2
|
||||
- Check passwords against known breach databases
|
||||
|
||||
**MITRE CWE-916**: Use of Password Hash With Insufficient Computational Effort. Assigned to systems using MD5, SHA1, or SHA256 for password storage without a slow key derivation function.
|
||||
|
||||
## Testing Your Understanding
|
||||
|
||||
1. An attacker steals a database with 1 million SHA256 hashes, all unsalted. How does the lack of salt help the attacker beyond just rainbow tables?
|
||||
|
||||
2. A website requires passwords to be "at least 8 characters with uppercase, lowercase, number, and special character." Why doesn't this prevent rule-based attacks?
|
||||
|
||||
3. Why does argon2 specifically resist GPU acceleration while bcrypt only partially resists it?
|
||||
|
||||
4. You find a hash `5f4dcc3b5aa765d61d8327deb882cf99` in a breach dump. Without running any cracking tool, what can you determine about it just from looking at it?
|
||||
|
||||
## Further Reading
|
||||
|
||||
**Essential:**
|
||||
- [How Passwords Are Stored](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) (OWASP)
|
||||
- [hashcat documentation](https://hashcat.net/wiki/) for understanding real world cracking techniques
|
||||
|
||||
**Deep Dive:**
|
||||
- [Bcrypt paper](https://www.usenix.org/legacy/events/usenix99/provos/provos.pdf) by Provos and Mazieres (1999)
|
||||
- [Argon2 specification](https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf) (Password Hashing Competition winner)
|
||||
- [Have I Been Pwned](https://haveibeenpwned.com/) for checking if passwords appear in breach databases
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
# System Architecture
|
||||
|
||||
This document breaks down how the system is designed and why certain architectural decisions were made.
|
||||
|
||||
## High Level Architecture
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ CLI Layer │ (main.cpp)
|
||||
│ - parse args │ Boost.program_options
|
||||
│ - detect hash │ Auto-type from length
|
||||
│ - dispatch │ Template instantiation
|
||||
└─────────┬──────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ Engine Layer │ (Engine.hpp)
|
||||
│ - create threads │ std::jthread pool
|
||||
│ - partition work │ Per-thread attack slices
|
||||
│ - coordinate │ Shared atomics
|
||||
│ - display │ Progress thread
|
||||
└─────────┬──────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
▼ ▼
|
||||
┌────────┐ ┌──────────┐
|
||||
│ Hasher │ │ Attack │
|
||||
│ Policy │ │ Strategy │
|
||||
└────────┘ └──────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────┐ ┌──────────────────────────────────┐
|
||||
│ EVP │ │ DictionaryAttack (mmap) │
|
||||
│ Hasher │ │ BruteForceAttack (keyspace math) │
|
||||
│ │ │ RuleAttack (dict + mutations) │
|
||||
└────────┘ └──────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Breakdown
|
||||
|
||||
**CLI Layer (main.cpp)**
|
||||
- Purpose: Parse command line arguments and dispatch to the correct Engine instantiation
|
||||
- Responsibilities: Argument validation, hash type detection, charset construction, JSON output
|
||||
- Key design choice: Uses a template lambda to avoid runtime polymorphism. The `switch` on hash type creates a compile-time-resolved call path
|
||||
|
||||
**Engine Layer (Engine.hpp)**
|
||||
- Purpose: Coordinate hashing, attack strategy, threading, and progress display
|
||||
- Responsibilities: Thread pool management, salt application, result collection, timing
|
||||
- Key design choice: Header-only template function. The hasher and attack strategy types are template parameters, so the compiler inlines the hash function directly into the cracking loop
|
||||
|
||||
**Hasher Policy (EVPHasher.hpp)**
|
||||
- Purpose: Compute cryptographic hashes via OpenSSL
|
||||
- Responsibilities: EVP context management, digest computation, hex encoding
|
||||
- Key design choice: Single template parameterized by algorithm function pointer. All four hash types are type aliases of the same implementation
|
||||
|
||||
**Attack Strategies (attack/)**
|
||||
- Purpose: Generate candidate passwords
|
||||
- Responsibilities: Wordlist reading, keyspace generation, mutation application, thread partitioning
|
||||
- Key design choice: Each strategy satisfies the `AttackStrategy` concept and handles its own partitioning via `create(path, thread_index, total_threads)`
|
||||
|
||||
**Progress Display (display/)**
|
||||
- Purpose: Show real-time cracking progress
|
||||
- Responsibilities: Progress bar rendering, speed/ETA calculation, result formatting
|
||||
- Key design choice: Runs on its own thread, reads shared atomics with relaxed ordering, no-ops when stdout isn't a terminal
|
||||
|
||||
## Core Design: Policy-Based Templates
|
||||
|
||||
The central architectural decision is resolving the hasher and attack strategy at compile time rather than runtime. Compare the two approaches:
|
||||
|
||||
**Runtime polymorphism (what we didn't do):**
|
||||
```cpp
|
||||
class IHasher {
|
||||
public:
|
||||
virtual std::string hash(std::string_view input) = 0;
|
||||
virtual ~IHasher() = default;
|
||||
};
|
||||
|
||||
void crack(IHasher* hasher, ...) {
|
||||
for (each candidate) {
|
||||
hasher->hash(candidate); // virtual call every iteration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every `hash()` call goes through the vtable, which means an indirect branch. The CPU can't inline the function body, can't optimize across the call boundary, and pays a branch prediction penalty. In a loop that runs millions of times per second, this adds up.
|
||||
|
||||
**Compile-time polymorphism (what we did):**
|
||||
```cpp
|
||||
template <Hasher H, AttackStrategy A>
|
||||
auto crack(const CrackConfig& cfg) -> std::expected<CrackResult, CrackError> {
|
||||
H hasher;
|
||||
// ... hasher.hash(candidate) is a direct call, gets inlined
|
||||
}
|
||||
```
|
||||
|
||||
When you write `Engine::crack<SHA256Hasher, DictionaryAttack>(cfg)`, the compiler generates a version of `crack` with SHA256Hasher hardcoded. The `hash()` call becomes a direct function call that gets inlined into the loop. No vtable, no indirection, no overhead.
|
||||
|
||||
The tradeoff: you get a separate copy of the function for each hasher/attack combination (12 total: 4 hashers x 3 attacks). This increases binary size slightly. For a CLI tool, that's irrelevant.
|
||||
|
||||
## Concepts as Contracts
|
||||
|
||||
C++20 concepts define what a Hasher or AttackStrategy must provide:
|
||||
|
||||
```cpp
|
||||
template <typename T>
|
||||
concept Hasher = requires(T h, std::string_view input) {
|
||||
{ h.hash(input) } -> std::same_as<std::string>;
|
||||
{ T::name() } -> std::convertible_to<std::string_view>;
|
||||
{ T::digest_length() } -> std::same_as<std::size_t>;
|
||||
};
|
||||
```
|
||||
|
||||
This is a compile-time contract. If you write a new hasher that doesn't satisfy `Hasher`, you get a clear error message at compile time instead of a mysterious linker error or runtime crash. It documents the interface without requiring inheritance.
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Dictionary Attack Flow
|
||||
|
||||
```
|
||||
1. CLI parses --hash and --wordlist
|
||||
main.cpp dispatches Engine::crack<SHA256Hasher, DictionaryAttack>(cfg)
|
||||
|
||||
2. Engine resolves thread count (hardware_concurrency if 0)
|
||||
Creates ThreadPool, spawns N jthreads
|
||||
|
||||
3. Each thread calls DictionaryAttack::create(path, thread_id, N)
|
||||
create() opens the file with mmap, counts lines,
|
||||
computes this thread's byte range [start_offset, end_offset)
|
||||
|
||||
4. Thread loop:
|
||||
a. attack.next() reads the next word from the mmap region
|
||||
b. If salt is set, prepend/append it to the candidate
|
||||
c. hasher.hash(candidate) computes the digest
|
||||
d. Compare to target hash
|
||||
e. If match: set atomic found flag, store result, break
|
||||
f. Increment local counter, flush to shared atomic every 1024 iterations
|
||||
|
||||
5. Display thread wakes every 100ms, reads shared atomics,
|
||||
renders progress bar to terminal
|
||||
|
||||
6. All threads join (jthread RAII)
|
||||
Engine returns CrackResult or CrackError::Exhausted
|
||||
```
|
||||
|
||||
### Brute Force Partitioning
|
||||
|
||||
The total keyspace for a charset of size C and max length L is:
|
||||
|
||||
```
|
||||
total = C^1 + C^2 + C^3 + ... + C^L
|
||||
```
|
||||
|
||||
Each thread gets a contiguous slice of the flat index space. Thread 0 gets indices `[0, total/N)`, thread 1 gets `[total/N, 2*total/N)`, and so on. Converting a flat index to a candidate string uses mixed-radix decomposition, similar to converting a decimal number to an arbitrary base but with variable-length output.
|
||||
|
||||
This means threads never communicate during the cracking loop. No shared queue, no work stealing, no locks. Each thread is completely independent.
|
||||
|
||||
## Threading Model
|
||||
|
||||
### Shared State
|
||||
|
||||
Only two values are shared between threads:
|
||||
|
||||
```cpp
|
||||
struct SharedState {
|
||||
alignas(64) std::atomic<bool> found{false};
|
||||
alignas(64) std::atomic<std::size_t> tested_count{0};
|
||||
std::mutex result_mutex;
|
||||
std::optional<std::string> result;
|
||||
};
|
||||
```
|
||||
|
||||
The `alignas(64)` places each atomic on its own cache line. Without this, writes to `tested_count` from one thread would invalidate reads of `found` on other threads because they share a 64-byte cache line. This is called false sharing and can cause a 5x slowdown.
|
||||
|
||||
### Counter Batching
|
||||
|
||||
Instead of doing an atomic increment on every candidate:
|
||||
|
||||
```cpp
|
||||
// Bad: atomic write every iteration (cross-core cache traffic)
|
||||
state.tested_count.fetch_add(1, std::memory_order_relaxed);
|
||||
```
|
||||
|
||||
Each thread maintains a local counter and flushes every 1024 iterations:
|
||||
|
||||
```cpp
|
||||
++local_count;
|
||||
if ((local_count & 0x3FF) == 0) {
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
local_count = 0;
|
||||
}
|
||||
```
|
||||
|
||||
This reduces cross-core atomic traffic by 1024x. The progress display reads an approximate count, which is fine for a UI update every 100ms.
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
All fallible operations return `std::expected<T, CrackError>`. There are no exceptions in the codebase.
|
||||
|
||||
```cpp
|
||||
enum class CrackError {
|
||||
FileNotFound,
|
||||
InvalidHash,
|
||||
UnsupportedAlgorithm,
|
||||
OpenSSLError,
|
||||
InvalidConfig,
|
||||
Exhausted
|
||||
};
|
||||
```
|
||||
|
||||
The error type is in the function signature. Callers are forced to handle it:
|
||||
|
||||
```cpp
|
||||
auto attack = DictionaryAttack::create(path, tid, total);
|
||||
if (!attack.has_value()) { return; }
|
||||
```
|
||||
|
||||
This makes error paths explicit and visible. You never have to guess whether a function might throw.
|
||||
|
||||
## Memory-Mapped I/O
|
||||
|
||||
DictionaryAttack uses `mmap` instead of `std::ifstream` for reading wordlists:
|
||||
|
||||
```cpp
|
||||
auto* mapped = static_cast<const char*>(
|
||||
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
||||
```
|
||||
|
||||
The file's contents are mapped directly into the process address space. Reading a word is pointer arithmetic (advance to the next newline). No `read()` syscalls, no kernel-to-user buffer copies. For a 140MB wordlist like rockyou.txt, this matters.
|
||||
|
||||
The `MappedFile` RAII wrapper handles cleanup:
|
||||
|
||||
```cpp
|
||||
class MappedFile {
|
||||
~MappedFile() {
|
||||
munmap(data_, size_);
|
||||
close(fd_);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
DictionaryAttack holds a `MappedFile` member. The compiler generates correct move operations automatically (Rule of Zero).
|
||||
|
||||
## Configuration
|
||||
|
||||
All magic numbers live in `Config.hpp`:
|
||||
|
||||
```cpp
|
||||
namespace config {
|
||||
constexpr unsigned DEFAULT_THREAD_COUNT = 0;
|
||||
constexpr std::size_t DEFAULT_MAX_BRUTE_LENGTH = 6;
|
||||
constexpr int PROGRESS_UPDATE_MS = 100;
|
||||
constexpr std::string_view CHARSET_LOWER = "abcdefghijklmnopqrstuvwxyz";
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Runtime configuration flows through `CrackConfig`, populated by the CLI parser and passed to the Engine. No globals are mutated after startup.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
**Hot path**: The inner loop in `Engine.hpp` is hash-and-compare. Every microsecond saved here multiplies by millions of iterations. The EVPHasher uses a lookup-table hex encoder instead of `std::ostringstream`, avoiding heap allocation per hash.
|
||||
|
||||
**Bottleneck**: On CPU, the bottleneck is the hash computation itself (OpenSSL's EVP internals). The surrounding code (candidate generation, comparison, counter update) is negligible in comparison. GPU acceleration (CUDA/OpenCL) would be the next step for real performance gains.
|
||||
|
||||
**Memory**: mmap means the wordlist is paged in on demand by the kernel. The tool's actual memory footprint is small regardless of wordlist size.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| Decision | Alternative | Why This Way |
|
||||
|----------|------------|-------------|
|
||||
| Template policies | Virtual interfaces | Zero overhead in hot loop |
|
||||
| `std::expected` | Exceptions | Explicit error paths, no hidden control flow |
|
||||
| `std::generator` | Return `vector<string>` | Lazy evaluation, early termination |
|
||||
| mmap | `std::ifstream` | Zero-copy, no syscall per line |
|
||||
| Work partitioning | Work stealing queue | Zero contention between threads |
|
||||
| Relaxed atomics | seq_cst | Approximate progress is fine, saves fence cost |
|
||||
| Header-only Engine | Compiled .cpp | Template must be in header anyway |
|
||||
|
||||
## Extensibility
|
||||
|
||||
**Adding a new hash algorithm:**
|
||||
1. Add a type alias in a new header:
|
||||
```cpp
|
||||
using SHA3_256Hasher = EVPHasher<EVP_sha3_256, "SHA3-256", 64>;
|
||||
```
|
||||
2. Add a case in `HashDetector::detect()` (if the digest length is unique)
|
||||
3. Add a case in `main.cpp`'s dispatch switch
|
||||
4. Write tests against known vectors
|
||||
|
||||
**Adding a new attack strategy:**
|
||||
1. Create a class satisfying the `AttackStrategy` concept (needs `next()`, `total()`, `progress()`)
|
||||
2. Add a `create(path, thread_index, total_threads)` factory
|
||||
3. Add a dispatch path in `main.cpp` and `Engine.hpp`
|
||||
|
||||
Both extension points require zero changes to the Engine itself.
|
||||
|
||||
## Limitations
|
||||
|
||||
- CPU only. No GPU acceleration. A dedicated tool like hashcat on a modern GPU is ~1000x faster
|
||||
- No bcrypt/scrypt/argon2 support. These require different libraries and fundamentally different cracking strategies (the intentional slowness changes the economics)
|
||||
- No distributed cracking. Each run uses one machine
|
||||
- The `--chain-rules` flag generates a large number of candidates per word, which can make rule attacks slow on large wordlists
|
||||
- Linux/macOS only (mmap is POSIX). Windows would need `CreateFileMapping` instead
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
# Implementation Walkthrough
|
||||
|
||||
This document walks through the actual code, explaining how each component works and why it's built the way it is.
|
||||
|
||||
## The Unified Hasher
|
||||
|
||||
The four hash algorithms (MD5, SHA1, SHA256, SHA512) all use the same OpenSSL EVP API. The only differences are which digest function to call and the output length. Instead of duplicating the implementation four times, a single template handles all of them:
|
||||
|
||||
```cpp
|
||||
template <auto Algorithm, auto Name, std::size_t DigestLen>
|
||||
class EVPHasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const;
|
||||
static constexpr std::string_view name() { return Name; }
|
||||
static constexpr std::size_t digest_length() { return DigestLen; }
|
||||
};
|
||||
```
|
||||
|
||||
Each concrete hasher is just a type alias:
|
||||
|
||||
```cpp
|
||||
using MD5Hasher = EVPHasher<EVP_md5, "MD5", 32>;
|
||||
using SHA256Hasher = EVPHasher<EVP_sha256, "SHA256", 64>;
|
||||
```
|
||||
|
||||
The template parameters are resolved at compile time, so the compiler generates four separate `hash()` implementations, each hardcoded to its specific digest function. Same performance as hand-written code, zero duplication.
|
||||
|
||||
### The Hot Path: Hash Computation
|
||||
|
||||
The `hash()` method is the most performance-critical function in the codebase. Every candidate password passes through it. The implementation uses OpenSSL's EVP interface with RAII cleanup:
|
||||
|
||||
```cpp
|
||||
std::string EVPHasher::hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
if (!ctx
|
||||
|| !EVP_DigestInit_ex(ctx.get(), Algorithm(), nullptr)
|
||||
|| !EVP_DigestUpdate(ctx.get(), input.data(), input.size())
|
||||
|| !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Convert digest bytes to hex string
|
||||
}
|
||||
```
|
||||
|
||||
The `unique_ptr` with a custom deleter (`EVP_MD_CTX_free`) ensures the context is freed even if something fails. The chained `if` checks every OpenSSL return value. Returning empty string on failure is the correct fail-safe: an empty string will never match a valid target hash, so a cracking run degrades gracefully instead of producing wrong results.
|
||||
|
||||
### Hex Encoding with a Lookup Table
|
||||
|
||||
The naive approach uses `std::ostringstream` with `std::hex` and `std::setw(2)`. That creates a heap-allocated stream object for every single hash. At millions of hashes per second, that's millions of unnecessary allocations.
|
||||
|
||||
Instead, a precomputed lookup table converts each byte to two hex characters with zero allocation:
|
||||
|
||||
```cpp
|
||||
static constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] {
|
||||
std::array<std::array<char, 2>, 256> t{};
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
t[i] = {digits[i >> 4], digits[i & 0xF]};
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
```
|
||||
|
||||
The table is computed at compile time (`constexpr` lambda). For each byte value 0-255, it stores the two hex characters. The high nibble (`i >> 4`) becomes the first character, the low nibble (`i & 0xF`) becomes the second. Byte `0xAB` maps to `{'a', 'b'}`.
|
||||
|
||||
Converting the full digest is a tight loop:
|
||||
|
||||
```cpp
|
||||
std::string hex(len * 2, '\0');
|
||||
for (unsigned int i = 0; i < len; ++i) {
|
||||
hex[i * 2] = HEX_TABLE[digest[i]][0];
|
||||
hex[i * 2 + 1] = HEX_TABLE[digest[i]][1];
|
||||
}
|
||||
```
|
||||
|
||||
One allocation for the output string (size known upfront), two array lookups per byte. This is the same approach hashcat uses.
|
||||
|
||||
## Hash Auto-Detection
|
||||
|
||||
`HashDetector::detect()` identifies the hash algorithm from the hex string:
|
||||
|
||||
```cpp
|
||||
std::expected<HashType, CrackError> HashDetector::detect(std::string_view hash) {
|
||||
// Validate all characters are hexadecimal
|
||||
if (!std::ranges::all_of(hash, is_hex)) {
|
||||
return std::unexpected(CrackError::InvalidHash);
|
||||
}
|
||||
|
||||
// Match length to algorithm
|
||||
switch (hash.size()) {
|
||||
case config::MD5_HEX_LENGTH: return HashType::MD5;
|
||||
case config::SHA1_HEX_LENGTH: return HashType::SHA1;
|
||||
case config::SHA256_HEX_LENGTH: return HashType::SHA256;
|
||||
case config::SHA512_HEX_LENGTH: return HashType::SHA512;
|
||||
default: return std::unexpected(CrackError::InvalidHash);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works because each algorithm produces a unique digest length. MD5 is always 32 hex characters, SHA1 is always 40, SHA256 is 64, SHA512 is 128. The hex validation catches typos and non-hash input before the cracking run wastes time.
|
||||
|
||||
## Dictionary Attack with mmap
|
||||
|
||||
### Memory Mapping the File
|
||||
|
||||
Instead of reading the wordlist line by line with `std::ifstream` (which copies data from kernel space to user space on every read), we map the entire file into the process address space:
|
||||
|
||||
```cpp
|
||||
auto* mapped = static_cast<const char*>(
|
||||
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
||||
```
|
||||
|
||||
After this call, `mapped` is a pointer to the file's contents in memory. Reading a word is just pointer arithmetic. The `MADV_SEQUENTIAL` hint tells the kernel to prefetch pages ahead of the current read position since we're scanning linearly.
|
||||
|
||||
### Thread Partitioning
|
||||
|
||||
For N threads, the file is split into N ranges by counting newlines:
|
||||
|
||||
```cpp
|
||||
std::size_t lines_per_thread = total_lines / total_threads;
|
||||
std::size_t my_start_line = thread_index * lines_per_thread + ...;
|
||||
```
|
||||
|
||||
Each thread walks forward through the file to find the byte offset of its starting line, then scans its range independently. No shared cursor, no locks, no coordination between threads during the cracking loop.
|
||||
|
||||
### Reading Words
|
||||
|
||||
The `next()` method scans from the current offset to the next newline:
|
||||
|
||||
```cpp
|
||||
std::expected<std::string, AttackComplete> DictionaryAttack::next() {
|
||||
while (current_offset_ < end_offset_) {
|
||||
// Find newline
|
||||
while (line_end < end_offset_ && file_.data()[line_end] != '\n') {
|
||||
++line_end;
|
||||
}
|
||||
|
||||
// Strip \r for Windows line endings
|
||||
if (word_end > line_start && file_.data()[word_end - 1] == '\r') {
|
||||
--word_end;
|
||||
}
|
||||
|
||||
// Skip empty lines (iterative, not recursive)
|
||||
if (word_end > line_start) {
|
||||
return std::string(file_.data() + line_start, word_end - line_start);
|
||||
}
|
||||
}
|
||||
return std::unexpected(AttackComplete{});
|
||||
}
|
||||
```
|
||||
|
||||
Empty lines are skipped iteratively. An earlier version used recursion (`return next()`), but a wordlist with many consecutive empty lines could theoretically overflow the stack.
|
||||
|
||||
## Brute Force Keyspace Generation
|
||||
|
||||
### Computing the Total Keyspace
|
||||
|
||||
For a charset of size C and max length L:
|
||||
|
||||
```cpp
|
||||
std::size_t compute_keyspace(std::size_t charset_size, std::size_t max_length) {
|
||||
std::size_t total = 0;
|
||||
std::size_t power = 1;
|
||||
for (std::size_t len = 1; len <= max_length; ++len) {
|
||||
power *= charset_size;
|
||||
total += power;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
This computes `C + C^2 + C^3 + ... + C^L`. For lowercase letters (C=26) and L=4, that's 26 + 676 + 17576 + 456976 = 475,254.
|
||||
|
||||
### Index-to-Candidate Conversion
|
||||
|
||||
Each candidate has a unique flat index in the range `[0, total)`. Converting an index to a string is like converting a number to a variable-length base:
|
||||
|
||||
```cpp
|
||||
std::string index_to_candidate(std::size_t index) const {
|
||||
// Determine which length bucket this index falls in
|
||||
std::size_t cumulative = 0;
|
||||
std::size_t power = base;
|
||||
std::size_t length = 1;
|
||||
while (cumulative + power <= index && length < max_length_) {
|
||||
cumulative += power;
|
||||
++length;
|
||||
power *= base;
|
||||
}
|
||||
|
||||
// Convert the offset within that bucket to characters
|
||||
std::size_t offset = index - cumulative;
|
||||
std::string result(length, charset_[0]);
|
||||
for (std::size_t i = length; i > 0; --i) {
|
||||
result[i - 1] = charset_[offset % base];
|
||||
offset /= base;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
This is the same algorithm as converting a decimal number to base-N, except the "digits" are characters from the charset. Index 0 maps to the first single-character string, and the indices increase through all single-character strings, then all two-character strings, and so on.
|
||||
|
||||
## Rule-Based Mutations with std::generator
|
||||
|
||||
### Individual Rules
|
||||
|
||||
Each rule is a coroutine that yields mutations lazily:
|
||||
|
||||
```cpp
|
||||
std::generator<std::string> RuleSet::leet_speak(std::string_view word) {
|
||||
std::string result(word);
|
||||
for (auto& c : result) {
|
||||
auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
for (auto [from, to] : LEET_MAP) {
|
||||
if (lower == from) { c = to; break; }
|
||||
}
|
||||
}
|
||||
co_yield std::move(result);
|
||||
}
|
||||
```
|
||||
|
||||
The `co_yield` keyword suspends the function and returns the value. When the caller asks for the next value, execution resumes where it left off. For simple rules like leet speak, there's only one yield. For append_digits, there are 1000 yields (one per digit 0-999).
|
||||
|
||||
### Composing Rules
|
||||
|
||||
`apply_all()` delegates to each rule using `std::ranges::elements_of`:
|
||||
|
||||
```cpp
|
||||
std::generator<std::string> RuleSet::apply_all(std::string_view word) {
|
||||
co_yield std::ranges::elements_of(capitalize_first(word));
|
||||
co_yield std::ranges::elements_of(uppercase_all(word));
|
||||
co_yield std::ranges::elements_of(leet_speak(word));
|
||||
co_yield std::ranges::elements_of(append_digits(word));
|
||||
co_yield std::ranges::elements_of(prepend_digits(word));
|
||||
co_yield std::ranges::elements_of(reverse(word));
|
||||
co_yield std::ranges::elements_of(toggle_case(word));
|
||||
}
|
||||
```
|
||||
|
||||
`elements_of` is a C++23 feature for delegating to sub-generators. Without it, you'd write `for (auto&& s : sub_gen) { co_yield std::move(s); }` for each rule, which is verbose and has O(depth) overhead per element.
|
||||
|
||||
### The Leet Map
|
||||
|
||||
The substitution table uses a constexpr array instead of `std::unordered_map`:
|
||||
|
||||
```cpp
|
||||
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {{
|
||||
{'a', '@'}, {'e', '3'}, {'i', '1'},
|
||||
{'o', '0'}, {'s', '$'}, {'t', '7'}
|
||||
}};
|
||||
```
|
||||
|
||||
Six entries. A linear scan over 6 elements is faster than computing a hash, looking up a bucket, and dereferencing a node pointer. The `unordered_map` would also heap-allocate at construction time, which the constexpr array avoids entirely.
|
||||
|
||||
## The Engine Template
|
||||
|
||||
The Engine wires everything together. It's a static template function in a header because it must be instantiated for each hasher/attack combination:
|
||||
|
||||
```cpp
|
||||
template <Hasher H, AttackStrategy A>
|
||||
auto Engine::crack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError>
|
||||
```
|
||||
|
||||
### Worker Lambda
|
||||
|
||||
Each thread runs a lambda that creates its own attack partition, its own hasher instance, and loops until it finds a match or runs out of candidates:
|
||||
|
||||
```cpp
|
||||
pool.run([&](unsigned tid, unsigned total, SharedState& state) {
|
||||
H hasher;
|
||||
auto attack = create_attack(); // partitioned for this thread
|
||||
|
||||
std::size_t local_count = 0;
|
||||
while (!state.found.load(std::memory_order_relaxed)) {
|
||||
auto candidate = attack->next();
|
||||
if (!candidate.has_value()) { break; }
|
||||
|
||||
std::string to_hash = *candidate;
|
||||
// Apply salt if configured...
|
||||
|
||||
if (hasher.hash(to_hash) == cfg.target_hash) {
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
state.set_result(std::move(*candidate));
|
||||
break;
|
||||
}
|
||||
|
||||
++local_count;
|
||||
if ((local_count & 0x3FF) == 0) {
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
local_count = 0;
|
||||
}
|
||||
}
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
});
|
||||
```
|
||||
|
||||
Key details:
|
||||
- `relaxed` memory ordering on the `found` flag is intentional. We don't need immediate visibility. If one thread sets `found` and another thread runs for a few extra iterations before seeing it, that's fine. The cost of stronger ordering (memory fences on every iteration) is not worth the guarantee of stopping instantly
|
||||
- The local counter batches atomic updates every 1024 iterations (`& 0x3FF` is a bitmask check, faster than modulo). The final `fetch_add` after the loop flushes any remaining count
|
||||
|
||||
## CLI Dispatch
|
||||
|
||||
The main function uses a template lambda to dispatch based on hash type without runtime polymorphism:
|
||||
|
||||
```cpp
|
||||
template <Hasher H>
|
||||
static auto dispatch_attack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError> {
|
||||
if (cfg.bruteforce) return Engine::crack<H, BruteForceAttack>(cfg);
|
||||
if (cfg.use_rules) return Engine::crack<H, RuleAttack>(cfg);
|
||||
return Engine::crack<H, DictionaryAttack>(cfg);
|
||||
}
|
||||
|
||||
static auto dispatch_hasher(HashType type, const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError> {
|
||||
switch (type) {
|
||||
case HashType::MD5: return dispatch_attack<MD5Hasher>(cfg);
|
||||
case HashType::SHA1: return dispatch_attack<SHA1Hasher>(cfg);
|
||||
case HashType::SHA256: return dispatch_attack<SHA256Hasher>(cfg);
|
||||
case HashType::SHA512: return dispatch_attack<SHA512Hasher>(cfg);
|
||||
}
|
||||
return std::unexpected(CrackError::UnsupportedAlgorithm);
|
||||
}
|
||||
```
|
||||
|
||||
The `switch` is the only point where a runtime decision is made. Each case instantiates the Engine template with a concrete hasher type. From that point forward, everything is resolved at compile time.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
The test suite has 38 tests organized by component:
|
||||
|
||||
**Hasher tests** verify against NIST known-answer vectors. If `SHA256Hasher::hash("password")` doesn't produce exactly `5e884898da28...`, the implementation is wrong. These are the most important tests because a subtle hashing bug would make the entire tool silently fail.
|
||||
|
||||
**HashDetector tests** verify type detection (length-based) and input validation (non-hex rejection).
|
||||
|
||||
**DictionaryAttack tests** verify word reading, thread partitioning (two threads together read all words), total count, and file-not-found error handling.
|
||||
|
||||
**BruteForceAttack tests** verify keyspace math, candidate generation completeness (all combinations produced), and thread partitioning (no duplicates, no gaps).
|
||||
|
||||
**RuleSet tests** verify each mutation rule against expected output. The `AllRulesProduceMutations` test confirms the total count exceeds 2000 (7 rules applied to "password", with append_digits and prepend_digits each producing 1000).
|
||||
|
||||
**Engine tests** are integration tests. `CracksSHA256WithDictionary` runs the full pipeline with 2 threads and verifies it finds "password". `CracksWithSalt` verifies salt prepending works end to end.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
**Forgetting to flush the local counter**: If a thread finds the password and breaks without flushing `local_count`, the final tested count will be wrong. The `fetch_add` after the loop handles this.
|
||||
|
||||
**Comparing hashes with different case**: SHA256 might produce uppercase hex on some platforms. Our hex encoder always produces lowercase, and the target hash is used as-is from the CLI. If someone pastes an uppercase hash, it won't match. A real production tool would normalize both to lowercase.
|
||||
|
||||
**mmap and file truncation**: If the wordlist file is modified while the tool is running, the behavior is undefined. `MAP_PRIVATE` helps (we get a copy-on-write snapshot), but for a tool that processes a file once and exits, this is not a practical concern.
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
# Extension Challenges
|
||||
|
||||
Ideas for extending this project, ordered by difficulty. Each one teaches a different skill. Don't feel like you need to do them in order.
|
||||
|
||||
## Easy Challenges
|
||||
|
||||
### 1. Add SHA3 Support
|
||||
|
||||
SHA3 (Keccak) is a completely different hash family from SHA2, based on a sponge construction instead of Merkle-Damgard. OpenSSL supports it through the same EVP API.
|
||||
|
||||
**What to build:** Add SHA3-256 and SHA3-512 hashers.
|
||||
|
||||
**What you'll learn:** How little code a new algorithm requires when the architecture is right. The EVPHasher template means this is a two-line change plus detection logic.
|
||||
|
||||
**Hints:**
|
||||
- `EVP_sha3_256()` and `EVP_sha3_512()` are the OpenSSL functions
|
||||
- SHA3-256 produces a 64-character hex digest (same length as SHA256), so auto-detection by length alone won't distinguish them. You'll need a `--type sha3-256` flag
|
||||
- Write tests against known vectors from the NIST SHA3 test suite
|
||||
|
||||
### 2. Batch Hash Cracking
|
||||
|
||||
Right now the tool cracks one hash at a time. Real breach dumps have millions of hashes.
|
||||
|
||||
**What to build:** Accept a file of hashes (one per line) and crack them all in a single run. Report which ones were cracked and which weren't.
|
||||
|
||||
**What you'll learn:** Amortizing dictionary reads across multiple targets. Instead of re-reading the wordlist for each hash, you hash each candidate once and compare against all targets simultaneously.
|
||||
|
||||
**Hints:**
|
||||
- Load all target hashes into a `std::unordered_set<std::string>`
|
||||
- For each candidate, hash it and check `targets.count(hash_result)`
|
||||
- This is how real cracking tools work. Cracking 1000 hashes is barely slower than cracking 1
|
||||
|
||||
### 3. Colored Hash Type Display
|
||||
|
||||
When auto-detecting, show the user what type was detected before cracking starts.
|
||||
|
||||
**What to build:** Add a colored line to the banner showing the detected algorithm with confidence indicator.
|
||||
|
||||
**What you'll learn:** Terminal UI design, ANSI escape sequences, and the ambiguity problem (SHA256 and SHA3-256 have the same digest length).
|
||||
|
||||
**Hints:**
|
||||
- Use the existing color constants in Config.hpp
|
||||
- Consider showing "SHA256 (auto-detected)" vs "SHA256 (specified)" so the user knows which path was taken
|
||||
|
||||
## Intermediate Challenges
|
||||
|
||||
### 4. Custom Rule File Format
|
||||
|
||||
The current rule set is hardcoded. Real cracking tools like hashcat and john support rule files where users define their own mutation patterns.
|
||||
|
||||
**What to build:** A rule file parser that reads rules from a text file:
|
||||
```
|
||||
: # do nothing (try the word as-is)
|
||||
c # capitalize first letter
|
||||
u # uppercase all
|
||||
l # lowercase all
|
||||
r # reverse
|
||||
$[0-9] # append digit
|
||||
^[0-9] # prepend digit
|
||||
sa@ # substitute a with @
|
||||
se3 # substitute e with 3
|
||||
```
|
||||
|
||||
**What you'll learn:** Language parsing, the hashcat rule format (which is a real industry standard), and how rule composition creates exponential candidate counts.
|
||||
|
||||
**Hints:**
|
||||
- Start with single-character rule codes, then add parametric rules like `$N` and `sXY`
|
||||
- hashcat's rule engine documentation describes the full syntax
|
||||
- A rule file with 50 rules applied to a 10K wordlist produces 500K candidates. That's still fast
|
||||
|
||||
### 5. Progress File for Resumable Cracking
|
||||
|
||||
If you're brute forcing an 8-character password and your machine crashes at 60% progress, you lose all that work.
|
||||
|
||||
**What to build:** Periodically save progress (current index, elapsed time, candidates tested) to a file. On restart with `--resume`, pick up where you left off.
|
||||
|
||||
**What you'll learn:** Checkpointing, atomic file writes (write to temp, rename), and the importance of deterministic work partitioning (our index-based brute force makes this easy since each index maps to exactly one candidate).
|
||||
|
||||
**Hints:**
|
||||
- For brute force, save the current flat index. That's all you need
|
||||
- For dictionary, save the byte offset in the file
|
||||
- Write the checkpoint every N seconds, not every N candidates (I/O is expensive relative to hashing)
|
||||
|
||||
### 6. Mask Attack
|
||||
|
||||
A mask attack is a smarter brute force. Instead of trying all characters in every position, you specify a pattern: `?u?l?l?l?d?d?d?d` means uppercase, three lowercase, four digits. This matches passwords like `Pass1234`.
|
||||
|
||||
**What to build:** A `--mask` flag that accepts hashcat-style mask syntax:
|
||||
```
|
||||
?l = lowercase ?u = uppercase
|
||||
?d = digit ?s = special
|
||||
?a = all A = literal 'A'
|
||||
```
|
||||
|
||||
**What you'll learn:** The massive efficiency gain from constrained search spaces. `?u?l?l?l?d?d?d?d` is 26*26*26*26*10*10*10*10 = 4.5 billion candidates. Full brute force of 8 characters from the same set is 218 trillion. That's a 48,000x reduction.
|
||||
|
||||
**Hints:**
|
||||
- Parse the mask into a vector of character sets, one per position
|
||||
- The keyspace calculation becomes `product of each position's charset size`
|
||||
- Partitioning works the same way as brute force (flat index, mixed-radix decomposition)
|
||||
|
||||
## Advanced Challenges
|
||||
|
||||
### 7. Rainbow Table Generator and Lookup
|
||||
|
||||
Rainbow tables are precomputed hash chains that trade disk space for cracking time. Instead of hashing every candidate at runtime, you build a table offline and do a lookup.
|
||||
|
||||
**What to build:** Two modes: `--generate-table` creates a rainbow table file for a given charset and length, `--rainbow` cracks using a precomputed table.
|
||||
|
||||
**What you'll learn:** The time-memory tradeoff in cryptanalysis, reduction functions, chain construction, and why salts make rainbow tables useless. This is one of the most elegant attacks in all of computer security.
|
||||
|
||||
**Hints:**
|
||||
- A rainbow table doesn't store every hash. It stores chains: start points and end points. Each chain covers thousands of hashes
|
||||
- The reduction function converts a hash back into a candidate (not a reverse of the hash, just a deterministic mapping)
|
||||
- Chain length controls the tradeoff: longer chains = smaller table but slower lookup
|
||||
- Start with a small example (4-char lowercase) to verify correctness before scaling up
|
||||
- Martin Hellman's original 1980 paper describes the concept. Philippe Oechslin's 2003 paper introduces the "rainbow" improvement
|
||||
|
||||
### 8. GPU-Accelerated Cracking with CUDA
|
||||
|
||||
The nuclear option. Move the hash-and-compare loop to the GPU.
|
||||
|
||||
**What to build:** A CUDA kernel that hashes candidates in parallel on the GPU. The CPU generates candidates and uploads batches; the GPU hashes thousands simultaneously.
|
||||
|
||||
**What you'll learn:** GPU programming, CUDA kernel design, host-device memory management, and why GPUs are so much faster at parallel computation (thousands of simple cores vs a few complex cores).
|
||||
|
||||
**Hints:**
|
||||
- You can't use OpenSSL on the GPU. Implement SHA256 in pure CUDA (the algorithm is public, about 100 lines of kernel code)
|
||||
- Upload candidates in batches (e.g., 1 million at a time) to amortize the host-to-device transfer cost
|
||||
- Use `cudaMemcpyAsync` with streams for overlapping computation and transfer
|
||||
- Start with SHA256 only. Getting one algorithm working on GPU is a significant accomplishment
|
||||
- This could be its own standalone advanced project in the repository
|
||||
|
||||
## Expert Challenge
|
||||
|
||||
### 9. Distributed Cracking Over the Network
|
||||
|
||||
Split the keyspace across multiple machines. One coordinator assigns work ranges; workers hash and report back.
|
||||
|
||||
**What to build:** A coordinator that accepts connections from worker nodes, assigns keyspace ranges, collects results, and handles worker failures (reassign their range to another worker).
|
||||
|
||||
**What you'll learn:** Distributed systems fundamentals: work distribution, heartbeating, fault tolerance, and the coordinator pattern. This is the same architecture that large-scale password cracking operations use.
|
||||
|
||||
**Hints:**
|
||||
- Use TCP sockets or gRPC for communication
|
||||
- The coordinator divides the total keyspace into chunks and assigns them on request
|
||||
- Workers send periodic heartbeats with their progress. If a worker goes silent, the coordinator reassigns its chunk
|
||||
- Think about what happens if two workers claim to crack the same hash (the coordinator should handle duplicate results gracefully)
|
||||
- This pairs well with Challenge 8 (each worker could be GPU-accelerated)
|
||||
|
||||
## Performance Challenges
|
||||
|
||||
### 10. Benchmark Suite
|
||||
|
||||
How fast is the tool actually? Compare different hash algorithms, threading configurations, and attack modes.
|
||||
|
||||
**What to build:** A benchmark mode (`--benchmark`) that runs standardized tests and reports results:
|
||||
```
|
||||
SHA256 dictionary (10K words): 2.4M h/s
|
||||
SHA256 brute force (6 chars): 2.1M h/s
|
||||
MD5 dictionary (10K words): 3.8M h/s
|
||||
SHA512 dictionary (10K words): 1.9M h/s
|
||||
Threads: 1=600K 2=1.2M 4=2.3M 8=2.4M
|
||||
```
|
||||
|
||||
**What you'll learn:** Microbenchmarking methodology, why results vary between runs, and where the actual bottlenecks are (hint: it's OpenSSL, not your code).
|
||||
|
||||
**Hints:**
|
||||
- Use `std::chrono::steady_clock` for timing
|
||||
- Run each benchmark multiple times and report median, not mean
|
||||
- Pin threads to specific cores with `pthread_setaffinity_np` for consistent results
|
||||
- Compare your numbers to hashcat's benchmarks to see the CPU vs GPU difference
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you get stuck on any challenge:
|
||||
|
||||
1. Read the relevant source code. The architecture is designed so each component is understandable in isolation
|
||||
2. Write a failing test first. If you can describe the expected behavior in a test, the implementation becomes clearer
|
||||
3. Start small. Get the simplest possible version working, then add complexity
|
||||
4. Check hashcat's documentation for real-world precedent. Most of these challenges are simplified versions of features that production cracking tools already implement
|
||||
|
|
@ -63,6 +63,22 @@ static auto dispatch_hasher(HashType type, const CrackConfig& cfg)
|
|||
return std::unexpected(CrackError::UnsupportedAlgorithm);
|
||||
}
|
||||
|
||||
static std::string json_escape(std::string_view s) {
|
||||
std::string result;
|
||||
result.reserve(s.size());
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': result += "\\\""; break;
|
||||
case '\\': result += "\\\\"; break;
|
||||
case '\n': result += "\\n"; break;
|
||||
case '\r': result += "\\r"; break;
|
||||
case '\t': result += "\\t"; break;
|
||||
default: result += c; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void write_json_result(const std::string& path,
|
||||
const CrackResult& result) {
|
||||
auto* f = std::fopen(path.c_str(), "w");
|
||||
|
|
@ -77,9 +93,9 @@ static void write_json_result(const std::string& path,
|
|||
" \"candidates_tested\": %zu,\n"
|
||||
" \"hashes_per_second\": %.2f\n"
|
||||
"}\n",
|
||||
result.plaintext.c_str(),
|
||||
result.hash.c_str(),
|
||||
result.algorithm.c_str(),
|
||||
json_escape(result.plaintext).c_str(),
|
||||
json_escape(result.hash).c_str(),
|
||||
json_escape(result.algorithm).c_str(),
|
||||
result.elapsed_seconds,
|
||||
result.candidates_tested,
|
||||
result.hashes_per_second);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static std::size_t count_lines_in_range(const char* data,
|
||||
std::size_t start,
|
||||
|
|
@ -31,36 +27,16 @@ static std::size_t find_next_newline(const char* data,
|
|||
|
||||
std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
|
||||
std::string_view path, unsigned thread_index, unsigned total_threads) {
|
||||
std::string path_str(path);
|
||||
int fd = open(path_str.c_str(), O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
auto file = MappedFile::open(path);
|
||||
if (!file.has_value()) {
|
||||
return std::unexpected(file.error());
|
||||
}
|
||||
|
||||
struct stat sb{};
|
||||
if (fstat(fd, &sb) < 0) {
|
||||
close(fd);
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
}
|
||||
auto* data = file->data();
|
||||
auto file_size = file->size();
|
||||
|
||||
auto file_size = static_cast<std::size_t>(sb.st_size);
|
||||
if (file_size == 0) {
|
||||
close(fd);
|
||||
return std::unexpected(CrackError::InvalidConfig);
|
||||
}
|
||||
|
||||
auto* mapped = static_cast<const char*>(
|
||||
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
|
||||
if (mapped == MAP_FAILED) {
|
||||
close(fd);
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
}
|
||||
|
||||
madvise(const_cast<char*>(mapped), file_size, MADV_SEQUENTIAL);
|
||||
|
||||
std::size_t total_lines = count_lines_in_range(mapped, 0, file_size);
|
||||
if (file_size > 0 && mapped[file_size - 1] != '\n') {
|
||||
std::size_t total_lines = count_lines_in_range(data, 0, file_size);
|
||||
if (file_size > 0 && data[file_size - 1] != '\n') {
|
||||
++total_lines;
|
||||
}
|
||||
|
||||
|
|
@ -74,18 +50,16 @@ std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
|
|||
|
||||
std::size_t start_offset = 0;
|
||||
for (std::size_t i = 0; i < my_start_line; ++i) {
|
||||
start_offset = find_next_newline(mapped, start_offset, file_size);
|
||||
start_offset = find_next_newline(data, start_offset, file_size);
|
||||
}
|
||||
|
||||
std::size_t end_offset = start_offset;
|
||||
for (std::size_t i = 0; i < my_line_count; ++i) {
|
||||
end_offset = find_next_newline(mapped, end_offset, file_size);
|
||||
end_offset = find_next_newline(data, end_offset, file_size);
|
||||
}
|
||||
|
||||
DictionaryAttack attack;
|
||||
attack.mapped_data_ = mapped;
|
||||
attack.file_size_ = file_size;
|
||||
attack.fd_ = fd;
|
||||
attack.file_ = std::move(*file);
|
||||
attack.start_offset_ = start_offset;
|
||||
attack.end_offset_ = end_offset;
|
||||
attack.current_offset_ = start_offset;
|
||||
|
|
@ -95,73 +69,28 @@ std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
|
|||
return attack;
|
||||
}
|
||||
|
||||
DictionaryAttack::~DictionaryAttack() {
|
||||
if (mapped_data_ && mapped_data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(mapped_data_), file_size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
close(fd_);
|
||||
}
|
||||
}
|
||||
|
||||
DictionaryAttack::DictionaryAttack(DictionaryAttack&& other) noexcept
|
||||
: mapped_data_(other.mapped_data_), file_size_(other.file_size_),
|
||||
fd_(other.fd_), start_offset_(other.start_offset_),
|
||||
end_offset_(other.end_offset_), current_offset_(other.current_offset_),
|
||||
total_words_(other.total_words_), words_read_(other.words_read_) {
|
||||
other.mapped_data_ = nullptr;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
DictionaryAttack& DictionaryAttack::operator=(DictionaryAttack&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (mapped_data_ && mapped_data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(mapped_data_), file_size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
close(fd_);
|
||||
}
|
||||
|
||||
mapped_data_ = other.mapped_data_;
|
||||
file_size_ = other.file_size_;
|
||||
fd_ = other.fd_;
|
||||
start_offset_ = other.start_offset_;
|
||||
end_offset_ = other.end_offset_;
|
||||
current_offset_ = other.current_offset_;
|
||||
total_words_ = other.total_words_;
|
||||
words_read_ = other.words_read_;
|
||||
|
||||
other.mapped_data_ = nullptr;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::expected<std::string, AttackComplete> DictionaryAttack::next() {
|
||||
if (current_offset_ >= end_offset_) {
|
||||
return std::unexpected(AttackComplete{});
|
||||
while (current_offset_ < end_offset_) {
|
||||
std::size_t line_start = current_offset_;
|
||||
std::size_t line_end = line_start;
|
||||
|
||||
while (line_end < end_offset_ && file_.data()[line_end] != '\n') {
|
||||
++line_end;
|
||||
}
|
||||
|
||||
std::size_t word_end = line_end;
|
||||
if (word_end > line_start && file_.data()[word_end - 1] == '\r') {
|
||||
--word_end;
|
||||
}
|
||||
|
||||
current_offset_ = (line_end < end_offset_) ? line_end + 1 : end_offset_;
|
||||
++words_read_;
|
||||
|
||||
if (word_end > line_start) {
|
||||
return std::string(file_.data() + line_start, word_end - line_start);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t line_start = current_offset_;
|
||||
std::size_t line_end = line_start;
|
||||
|
||||
while (line_end < end_offset_ && mapped_data_[line_end] != '\n') {
|
||||
++line_end;
|
||||
}
|
||||
|
||||
std::size_t word_end = line_end;
|
||||
if (word_end > line_start && mapped_data_[word_end - 1] == '\r') {
|
||||
--word_end;
|
||||
}
|
||||
|
||||
current_offset_ = (line_end < end_offset_) ? line_end + 1 : end_offset_;
|
||||
++words_read_;
|
||||
|
||||
if (word_end <= line_start) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return std::string(mapped_data_ + line_start, word_end - line_start);
|
||||
return std::unexpected(AttackComplete{});
|
||||
}
|
||||
|
||||
std::size_t DictionaryAttack::total() const { return total_words_; }
|
||||
|
|
|
|||
|
|
@ -7,18 +7,14 @@
|
|||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include "src/io/MappedFile.hpp"
|
||||
|
||||
class DictionaryAttack {
|
||||
public:
|
||||
static std::expected<DictionaryAttack, CrackError> create(
|
||||
std::string_view path, unsigned thread_index, unsigned total_threads);
|
||||
|
||||
~DictionaryAttack();
|
||||
DictionaryAttack(DictionaryAttack&& other) noexcept;
|
||||
DictionaryAttack& operator=(DictionaryAttack&& other) noexcept;
|
||||
|
||||
std::expected<std::string, AttackComplete> next();
|
||||
std::size_t total() const;
|
||||
std::size_t progress() const;
|
||||
|
|
@ -26,9 +22,7 @@ public:
|
|||
private:
|
||||
DictionaryAttack() = default;
|
||||
|
||||
const char* mapped_data_ = nullptr;
|
||||
std::size_t file_size_ = 0;
|
||||
int fd_ = -1;
|
||||
MappedFile file_;
|
||||
|
||||
std::size_t start_offset_ = 0;
|
||||
std::size_t end_offset_ = 0;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ bool RuleAttack::load_next_word() {
|
|||
mutations_.push_back(std::move(m));
|
||||
}
|
||||
|
||||
if (chain_rules_) {
|
||||
std::vector<std::string> base(mutations_.begin() + 1, mutations_.end());
|
||||
for (const auto& b : base) {
|
||||
for (auto&& m : RuleSet::apply_all(b)) {
|
||||
mutations_.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
auto attack = create_attack();
|
||||
if (!attack.has_value()) { return; }
|
||||
|
||||
std::size_t local_count = 0;
|
||||
while (!state.found.load(std::memory_order_relaxed)) {
|
||||
auto candidate = attack->next();
|
||||
if (!candidate.has_value()) { break; }
|
||||
|
|
@ -110,12 +111,18 @@ auto Engine::crack(const CrackConfig& cfg)
|
|||
}
|
||||
|
||||
if (hasher.hash(to_hash) == cfg.target_hash) {
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
state.set_result(std::move(*candidate));
|
||||
break;
|
||||
}
|
||||
|
||||
state.tested_count.fetch_add(1, std::memory_order_relaxed);
|
||||
++local_count;
|
||||
if ((local_count & 0x3FF) == 0) {
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
local_count = 0;
|
||||
}
|
||||
}
|
||||
state.tested_count.fetch_add(local_count, std::memory_order_relaxed);
|
||||
});
|
||||
|
||||
if (display_thread.joinable()) {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,14 @@
|
|||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static auto start_time = std::chrono::steady_clock::now();
|
||||
|
||||
Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
|
||||
unsigned thread_count, std::size_t total_candidates,
|
||||
const std::atomic<bool>& found,
|
||||
const std::atomic<std::size_t>& tested)
|
||||
: algorithm_(algorithm), attack_mode_(attack_mode),
|
||||
thread_count_(thread_count), total_(total_candidates),
|
||||
found_(found), tested_(tested) {
|
||||
start_time = std::chrono::steady_clock::now();
|
||||
}
|
||||
found_(found), tested_(tested),
|
||||
start_time_(std::chrono::steady_clock::now()) {}
|
||||
|
||||
bool Progress::is_tty() {
|
||||
return isatty(STDOUT_FILENO) != 0;
|
||||
|
|
@ -112,7 +109,7 @@ void Progress::update() {
|
|||
if (!is_tty()) { return; }
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time).count();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time_).count();
|
||||
auto tested_val = tested_.load(std::memory_order_relaxed);
|
||||
|
||||
double fraction = (total_ > 0)
|
||||
|
|
@ -178,7 +175,7 @@ void Progress::print_exhausted(std::string_view hash,
|
|||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time).count();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time_).count();
|
||||
auto tested_val = tested_.load(std::memory_order_relaxed);
|
||||
|
||||
std::print("\033[3A\033[J");
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
|
@ -31,6 +32,8 @@ private:
|
|||
const std::atomic<bool>& found_;
|
||||
const std::atomic<std::size_t>& tested_;
|
||||
|
||||
std::chrono::steady_clock::time_point start_time_;
|
||||
|
||||
static std::size_t terminal_width();
|
||||
std::string render_bar(double fraction, std::size_t width) const;
|
||||
static std::string format_count(std::size_t n);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
// ©AngelaMos | 2026
|
||||
// EVPHasher.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <openssl/evp.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace evp {
|
||||
|
||||
struct MD5Tag {
|
||||
static const EVP_MD* algorithm() { return EVP_md5(); }
|
||||
static constexpr std::string_view name = "MD5";
|
||||
static constexpr std::size_t hex_length = 32;
|
||||
};
|
||||
|
||||
struct SHA1Tag {
|
||||
static const EVP_MD* algorithm() { return EVP_sha1(); }
|
||||
static constexpr std::string_view name = "SHA1";
|
||||
static constexpr std::size_t hex_length = 40;
|
||||
};
|
||||
|
||||
struct SHA256Tag {
|
||||
static const EVP_MD* algorithm() { return EVP_sha256(); }
|
||||
static constexpr std::string_view name = "SHA256";
|
||||
static constexpr std::size_t hex_length = 64;
|
||||
};
|
||||
|
||||
struct SHA512Tag {
|
||||
static const EVP_MD* algorithm() { return EVP_sha512(); }
|
||||
static constexpr std::string_view name = "SHA512";
|
||||
static constexpr std::size_t hex_length = 128;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
inline constexpr std::array<std::array<char, 2>, 256> HEX_TABLE = [] {
|
||||
std::array<std::array<char, 2>, 256> t{};
|
||||
constexpr std::array<char, 17> digits = {
|
||||
'0','1','2','3','4','5','6','7',
|
||||
'8','9','a','b','c','d','e','f','\0'};
|
||||
for (std::size_t i = 0; i < 256; ++i) {
|
||||
t.at(i) = {digits.at(i >> 4), digits.at(i & 0xF)};
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
|
||||
template <typename Tag>
|
||||
class EVPHasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
|
||||
if (!ctx
|
||||
|| !EVP_DigestInit_ex(ctx.get(), Tag::algorithm(), nullptr)
|
||||
|| !EVP_DigestUpdate(ctx.get(), input.data(), input.size())
|
||||
|| !EVP_DigestFinal_ex(ctx.get(), digest.data(), &len)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string hex(static_cast<std::size_t>(len) * 2, '\0');
|
||||
for (std::size_t i = 0; i < len; ++i) {
|
||||
hex.at(i * 2) = HEX_TABLE.at(digest.at(i)).at(0);
|
||||
hex.at(i * 2 + 1) = HEX_TABLE.at(digest.at(i)).at(1);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
static constexpr std::string_view name() { return Tag::name; }
|
||||
static constexpr std::size_t digest_length() { return Tag::hex_length; }
|
||||
};
|
||||
|
||||
using MD5Hasher = EVPHasher<evp::MD5Tag>;
|
||||
using SHA1Hasher = EVPHasher<evp::SHA1Tag>;
|
||||
using SHA256Hasher = EVPHasher<evp::SHA256Tag>;
|
||||
using SHA512Hasher = EVPHasher<evp::SHA512Tag>;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// ©AngelaMos | 2026
|
||||
// MD5Hasher.cpp
|
||||
|
||||
#include "src/hash/MD5Hasher.hpp"
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <openssl/evp.h>
|
||||
#include <sstream>
|
||||
|
||||
std::string MD5Hasher::hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
EVP_DigestInit_ex(ctx.get(), EVP_md5(), nullptr);
|
||||
EVP_DigestUpdate(ctx.get(), input.data(), input.size());
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
EVP_DigestFinal_ex(ctx.get(), digest.data(), &len);
|
||||
|
||||
std::ostringstream hex;
|
||||
hex << std::hex << std::setfill('0');
|
||||
for (unsigned int i = 0; i < len; ++i) {
|
||||
hex << std::setw(2) << static_cast<int>(digest[i]);
|
||||
}
|
||||
return hex.str();
|
||||
}
|
||||
|
|
@ -2,14 +2,4 @@
|
|||
// MD5Hasher.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class MD5Hasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const;
|
||||
static constexpr std::string_view name() { return "MD5"; }
|
||||
static constexpr std::size_t digest_length() { return 32; }
|
||||
};
|
||||
#include "src/hash/EVPHasher.hpp"
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// ©AngelaMos | 2026
|
||||
// SHA1Hasher.cpp
|
||||
|
||||
#include "src/hash/SHA1Hasher.hpp"
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <openssl/evp.h>
|
||||
#include <sstream>
|
||||
|
||||
std::string SHA1Hasher::hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
EVP_DigestInit_ex(ctx.get(), EVP_sha1(), nullptr);
|
||||
EVP_DigestUpdate(ctx.get(), input.data(), input.size());
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
EVP_DigestFinal_ex(ctx.get(), digest.data(), &len);
|
||||
|
||||
std::ostringstream hex;
|
||||
hex << std::hex << std::setfill('0');
|
||||
for (unsigned int i = 0; i < len; ++i) {
|
||||
hex << std::setw(2) << static_cast<int>(digest[i]);
|
||||
}
|
||||
return hex.str();
|
||||
}
|
||||
|
|
@ -2,14 +2,4 @@
|
|||
// SHA1Hasher.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class SHA1Hasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const;
|
||||
static constexpr std::string_view name() { return "SHA1"; }
|
||||
static constexpr std::size_t digest_length() { return 40; }
|
||||
};
|
||||
#include "src/hash/EVPHasher.hpp"
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// ©AngelaMos | 2026
|
||||
// SHA256Hasher.cpp
|
||||
|
||||
#include "src/hash/SHA256Hasher.hpp"
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <openssl/evp.h>
|
||||
#include <sstream>
|
||||
|
||||
std::string SHA256Hasher::hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr);
|
||||
EVP_DigestUpdate(ctx.get(), input.data(), input.size());
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
EVP_DigestFinal_ex(ctx.get(), digest.data(), &len);
|
||||
|
||||
std::ostringstream hex;
|
||||
hex << std::hex << std::setfill('0');
|
||||
for (unsigned int i = 0; i < len; ++i) {
|
||||
hex << std::setw(2) << static_cast<int>(digest[i]);
|
||||
}
|
||||
return hex.str();
|
||||
}
|
||||
|
|
@ -2,14 +2,4 @@
|
|||
// SHA256Hasher.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class SHA256Hasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const;
|
||||
static constexpr std::string_view name() { return "SHA256"; }
|
||||
static constexpr std::size_t digest_length() { return 64; }
|
||||
};
|
||||
#include "src/hash/EVPHasher.hpp"
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// ©AngelaMos | 2026
|
||||
// SHA512Hasher.cpp
|
||||
|
||||
#include "src/hash/SHA512Hasher.hpp"
|
||||
#include <array>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <openssl/evp.h>
|
||||
#include <sstream>
|
||||
|
||||
std::string SHA512Hasher::hash(std::string_view input) const {
|
||||
auto ctx = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
||||
EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
|
||||
EVP_DigestInit_ex(ctx.get(), EVP_sha512(), nullptr);
|
||||
EVP_DigestUpdate(ctx.get(), input.data(), input.size());
|
||||
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
|
||||
unsigned int len = 0;
|
||||
EVP_DigestFinal_ex(ctx.get(), digest.data(), &len);
|
||||
|
||||
std::ostringstream hex;
|
||||
hex << std::hex << std::setfill('0');
|
||||
for (unsigned int i = 0; i < len; ++i) {
|
||||
hex << std::setw(2) << static_cast<int>(digest[i]);
|
||||
}
|
||||
return hex.str();
|
||||
}
|
||||
|
|
@ -2,14 +2,4 @@
|
|||
// SHA512Hasher.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class SHA512Hasher {
|
||||
public:
|
||||
std::string hash(std::string_view input) const;
|
||||
static constexpr std::string_view name() { return "SHA512"; }
|
||||
static constexpr std::size_t digest_length() { return 128; }
|
||||
};
|
||||
#include "src/hash/EVPHasher.hpp"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// ©AngelaMos | 2026
|
||||
// MappedFile.cpp
|
||||
|
||||
#include "src/io/MappedFile.hpp"
|
||||
#include <fcntl.h>
|
||||
#include <string>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
std::expected<MappedFile, CrackError> MappedFile::open(std::string_view path) {
|
||||
std::string path_str(path);
|
||||
int fd = ::open(path_str.c_str(), O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
}
|
||||
|
||||
struct stat sb{};
|
||||
if (fstat(fd, &sb) < 0) {
|
||||
::close(fd);
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
}
|
||||
|
||||
auto file_size = static_cast<std::size_t>(sb.st_size);
|
||||
if (file_size == 0) {
|
||||
::close(fd);
|
||||
return std::unexpected(CrackError::InvalidConfig);
|
||||
}
|
||||
|
||||
auto* mapped = static_cast<const char*>(
|
||||
mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0));
|
||||
|
||||
if (mapped == MAP_FAILED) {
|
||||
::close(fd);
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
}
|
||||
|
||||
madvise(const_cast<char*>(mapped), file_size, MADV_SEQUENTIAL);
|
||||
|
||||
MappedFile mf;
|
||||
mf.data_ = mapped;
|
||||
mf.size_ = file_size;
|
||||
mf.fd_ = fd;
|
||||
return mf;
|
||||
}
|
||||
|
||||
MappedFile::~MappedFile() {
|
||||
if (data_ && data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(data_), size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
}
|
||||
}
|
||||
|
||||
MappedFile::MappedFile(MappedFile&& other) noexcept
|
||||
: data_(other.data_), size_(other.size_), fd_(other.fd_) {
|
||||
other.data_ = nullptr;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
|
||||
MappedFile& MappedFile::operator=(MappedFile&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (data_ && data_ != MAP_FAILED) {
|
||||
munmap(const_cast<char*>(data_), size_);
|
||||
}
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
}
|
||||
data_ = other.data_;
|
||||
size_ = other.size_;
|
||||
fd_ = other.fd_;
|
||||
other.data_ = nullptr;
|
||||
other.fd_ = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// ©AngelaMos | 2026
|
||||
// MappedFile.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <expected>
|
||||
#include <string_view>
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class MappedFile {
|
||||
public:
|
||||
MappedFile() = default;
|
||||
static std::expected<MappedFile, CrackError> open(std::string_view path);
|
||||
|
||||
~MappedFile();
|
||||
MappedFile(MappedFile&& other) noexcept;
|
||||
MappedFile& operator=(MappedFile&& other) noexcept;
|
||||
|
||||
MappedFile(const MappedFile&) = delete;
|
||||
MappedFile& operator=(const MappedFile&) = delete;
|
||||
|
||||
const char* data() const { return data_; }
|
||||
std::size_t size() const { return size_; }
|
||||
|
||||
private:
|
||||
const char* data_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
int fd_ = -1;
|
||||
};
|
||||
|
|
@ -4,13 +4,15 @@
|
|||
#include "src/rules/RuleSet.hpp"
|
||||
#include "src/config/Config.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <unordered_map>
|
||||
#include <ranges>
|
||||
#include <utility>
|
||||
|
||||
static const std::unordered_map<char, char> LEET_MAP = {
|
||||
static constexpr std::array<std::pair<char, char>, 6> LEET_MAP = {{
|
||||
{'a', '@'}, {'e', '3'}, {'i', '1'},
|
||||
{'o', '0'}, {'s', '$'}, {'t', '7'}
|
||||
};
|
||||
}};
|
||||
|
||||
std::generator<std::string> RuleSet::capitalize_first(std::string_view word) {
|
||||
if (word.empty()) { co_return; }
|
||||
|
|
@ -30,10 +32,9 @@ std::generator<std::string> RuleSet::uppercase_all(std::string_view word) {
|
|||
std::generator<std::string> RuleSet::leet_speak(std::string_view word) {
|
||||
std::string result(word);
|
||||
for (auto& c : result) {
|
||||
auto it = LEET_MAP.find(static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(c))));
|
||||
if (it != LEET_MAP.end()) {
|
||||
c = it->second;
|
||||
auto lower = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
for (auto [from, to] : LEET_MAP) {
|
||||
if (lower == from) { c = to; break; }
|
||||
}
|
||||
}
|
||||
co_yield std::move(result);
|
||||
|
|
@ -68,11 +69,11 @@ std::generator<std::string> RuleSet::toggle_case(std::string_view word) {
|
|||
}
|
||||
|
||||
std::generator<std::string> RuleSet::apply_all(std::string_view word) {
|
||||
for (auto&& s : capitalize_first(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : uppercase_all(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : leet_speak(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : append_digits(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : prepend_digits(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : reverse(word)) { co_yield std::move(s); }
|
||||
for (auto&& s : toggle_case(word)) { co_yield std::move(s); }
|
||||
co_yield std::ranges::elements_of(capitalize_first(word));
|
||||
co_yield std::ranges::elements_of(uppercase_all(word));
|
||||
co_yield std::ranges::elements_of(leet_speak(word));
|
||||
co_yield std::ranges::elements_of(append_digits(word));
|
||||
co_yield std::ranges::elements_of(prepend_digits(word));
|
||||
co_yield std::ranges::elements_of(reverse(word));
|
||||
co_yield std::ranges::elements_of(toggle_case(word));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
#include <vector>
|
||||
|
||||
struct SharedState {
|
||||
std::atomic<bool> found{false};
|
||||
std::atomic<std::size_t> tested_count{0};
|
||||
alignas(64) std::atomic<bool> found{false};
|
||||
alignas(64) std::atomic<std::size_t> tested_count{0};
|
||||
std::mutex result_mutex;
|
||||
std::optional<std::string> result;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,3 +65,10 @@ TEST(SHA512HasherTest, StaticProperties) {
|
|||
EXPECT_EQ(SHA512Hasher::name(), "SHA512");
|
||||
EXPECT_EQ(SHA512Hasher::digest_length(), 128);
|
||||
}
|
||||
|
||||
TEST(HasherTest, NeverReturnsEmpty) {
|
||||
EXPECT_FALSE(MD5Hasher{}.hash("test").empty());
|
||||
EXPECT_FALSE(SHA1Hasher{}.hash("test").empty());
|
||||
EXPECT_FALSE(SHA256Hasher{}.hash("test").empty());
|
||||
EXPECT_FALSE(SHA512Hasher{}.hash("test").empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,3 +79,18 @@ TEST(RuleAttackTest, AppliesRulesToDictionaryWords) {
|
|||
EXPECT_TRUE(std::ranges::find(candidates, "p@$$w0rd") != candidates.end());
|
||||
EXPECT_TRUE(std::ranges::find(candidates, "password") != candidates.end());
|
||||
}
|
||||
|
||||
TEST(RuleAttackTest, ChainRulesProducesMoreCandidates) {
|
||||
auto without = RuleAttack::create("tests/data/small_wordlist.txt", false, 0, 1);
|
||||
auto with_chain = RuleAttack::create("tests/data/small_wordlist.txt", true, 0, 1);
|
||||
ASSERT_TRUE(without.has_value());
|
||||
ASSERT_TRUE(with_chain.has_value());
|
||||
|
||||
std::size_t count_without = 0;
|
||||
while (without->next()) { ++count_without; }
|
||||
|
||||
std::size_t count_with = 0;
|
||||
while (with_chain->next()) { ++count_with; }
|
||||
|
||||
EXPECT_GT(count_with, count_without);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ package pyproject
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ func (u *Updater) Bytes() []byte {
|
|||
|
||||
// WriteFile atomically writes the updated content to disk
|
||||
func (u *Updater) WriteFile(path string) error {
|
||||
path = filepath.Clean(path)
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, u.content, 0o600); err != nil {
|
||||
return fmt.Errorf("write temp: %w", err)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ package requirements
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
|
|
@ -52,6 +53,7 @@ func UpdateFile(path string, updates map[string]string) error {
|
|||
}
|
||||
}
|
||||
|
||||
path = filepath.Clean(path)
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, content, 0o600); err != nil {
|
||||
return fmt.Errorf("write temp: %w", err)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0eec274fba59ddd373080ae961dd4497eaf2e454
|
||||
Subproject commit ecbb534e85e8e381e6e89aece4b786db8f7ad172
|
||||
Loading…
Reference in New Issue