feat: Engine, ThreadPool, and Progress display
ThreadPool: jthread-based work partitioning with SharedState atomics. Progress: rich terminal output with Unicode box drawing, ANSI colors, progress bar, speed/ETA display, and cracked/exhausted result rendering. Engine: template function wiring hasher + attack + threading + salt. 3 integration tests passing (crack, exhaust, salted crack).
This commit is contained in:
parent
6542a097f8
commit
3e2c3ff002
|
|
@ -0,0 +1,148 @@
|
|||
// ©AngelaMos | 2026
|
||||
// Engine.hpp
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
#include "src/config/Config.hpp"
|
||||
#include "src/core/Concepts.hpp"
|
||||
#include "src/display/Progress.hpp"
|
||||
#include "src/threading/ThreadPool.hpp"
|
||||
|
||||
class Engine {
|
||||
public:
|
||||
template <Hasher H, AttackStrategy A>
|
||||
static auto crack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError>;
|
||||
};
|
||||
|
||||
template <Hasher H, AttackStrategy A>
|
||||
auto Engine::crack(const CrackConfig& cfg)
|
||||
-> std::expected<CrackResult, CrackError> {
|
||||
unsigned thread_count = cfg.thread_count > 0
|
||||
? cfg.thread_count
|
||||
: std::thread::hardware_concurrency();
|
||||
|
||||
ThreadPool pool(thread_count);
|
||||
|
||||
auto attack_name = [&]() -> std::string_view {
|
||||
if (cfg.bruteforce) { return "Brute Force"; }
|
||||
if (cfg.use_rules) { return "Rules"; }
|
||||
return "Dictionary";
|
||||
}();
|
||||
|
||||
auto total_estimate = [&]() -> std::size_t {
|
||||
if constexpr (std::same_as<A, BruteForceAttack>) {
|
||||
BruteForceAttack probe(cfg.charset, cfg.max_length, 0, 1);
|
||||
return probe.total();
|
||||
} else if constexpr (std::same_as<A, RuleAttack>) {
|
||||
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
|
||||
if (!probe) { return 0; }
|
||||
return probe->total() * 2005;
|
||||
} else {
|
||||
auto probe = DictionaryAttack::create(cfg.wordlist_path, 0, 1);
|
||||
if (!probe) { return 0; }
|
||||
return probe->total();
|
||||
}
|
||||
}();
|
||||
|
||||
Progress progress(H::name(), attack_name, thread_count,
|
||||
total_estimate, pool.state().found,
|
||||
pool.state().tested_count);
|
||||
|
||||
if (Progress::is_tty()) {
|
||||
progress.print_banner();
|
||||
std::puts("");
|
||||
std::puts("");
|
||||
std::puts("");
|
||||
}
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
std::jthread display_thread;
|
||||
if (Progress::is_tty()) {
|
||||
display_thread = std::jthread([&](std::stop_token st) {
|
||||
while (!st.stop_requested() &&
|
||||
!pool.state().found.load(std::memory_order_relaxed)) {
|
||||
progress.update();
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(config::PROGRESS_UPDATE_MS));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pool.run([&](unsigned tid, unsigned total, SharedState& state) {
|
||||
H hasher;
|
||||
|
||||
auto create_attack = [&]() {
|
||||
if constexpr (std::same_as<A, BruteForceAttack>) {
|
||||
return std::expected<BruteForceAttack, CrackError>(
|
||||
BruteForceAttack(cfg.charset, cfg.max_length, tid, total));
|
||||
} else if constexpr (std::same_as<A, RuleAttack>) {
|
||||
return RuleAttack::create(
|
||||
cfg.wordlist_path, cfg.chain_rules, tid, total);
|
||||
} else {
|
||||
return DictionaryAttack::create(cfg.wordlist_path, tid, total);
|
||||
}
|
||||
};
|
||||
|
||||
auto attack = create_attack();
|
||||
if (!attack.has_value()) { return; }
|
||||
|
||||
while (!state.found.load(std::memory_order_relaxed)) {
|
||||
auto candidate = attack->next();
|
||||
if (!candidate.has_value()) { break; }
|
||||
|
||||
std::string to_hash = *candidate;
|
||||
if (!cfg.salt.empty()) {
|
||||
if (cfg.salt_position == "prepend") {
|
||||
to_hash = cfg.salt + to_hash;
|
||||
} else {
|
||||
to_hash = to_hash + cfg.salt;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasher.hash(to_hash) == cfg.target_hash) {
|
||||
state.set_result(std::move(*candidate));
|
||||
break;
|
||||
}
|
||||
|
||||
state.tested_count.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
});
|
||||
|
||||
if (display_thread.joinable()) {
|
||||
display_thread.request_stop();
|
||||
display_thread.join();
|
||||
}
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(end - start).count();
|
||||
auto tested = pool.state().tested_count.load(std::memory_order_relaxed);
|
||||
double speed = (elapsed > 0.0) ? static_cast<double>(tested) / elapsed : 0.0;
|
||||
|
||||
auto& state = pool.state();
|
||||
if (state.found.load(std::memory_order_relaxed) && state.result.has_value()) {
|
||||
CrackResult result{
|
||||
.plaintext = *state.result,
|
||||
.hash = cfg.target_hash,
|
||||
.algorithm = std::string(H::name()),
|
||||
.elapsed_seconds = elapsed,
|
||||
.candidates_tested = tested,
|
||||
.hashes_per_second = speed
|
||||
};
|
||||
|
||||
progress.print_cracked(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
progress.print_exhausted(cfg.target_hash, H::name());
|
||||
return std::unexpected(CrackError::Exhausted);
|
||||
}
|
||||
|
|
@ -3,6 +3,12 @@
|
|||
|
||||
#include "src/display/Progress.hpp"
|
||||
#include "src/config/Config.hpp"
|
||||
#include <chrono>
|
||||
#include <print>
|
||||
#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,
|
||||
|
|
@ -10,15 +16,178 @@ Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
|
|||
const std::atomic<std::size_t>& tested)
|
||||
: algorithm_(algorithm), attack_mode_(attack_mode),
|
||||
thread_count_(thread_count), total_(total_candidates),
|
||||
found_(found), tested_(tested) {}
|
||||
found_(found), tested_(tested) {
|
||||
start_time = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void Progress::print_banner() const {}
|
||||
void Progress::update() {}
|
||||
void Progress::print_cracked(const CrackResult&) const {}
|
||||
void Progress::print_exhausted(std::string_view, std::string_view) const {}
|
||||
bool Progress::is_tty() { return false; }
|
||||
std::size_t Progress::terminal_width() { return 80; }
|
||||
std::string Progress::render_bar(double, std::size_t) const { return ""; }
|
||||
std::string Progress::format_count(std::size_t) { return ""; }
|
||||
std::string Progress::format_time(double) { return ""; }
|
||||
std::string Progress::format_speed(double) { return ""; }
|
||||
bool Progress::is_tty() {
|
||||
return isatty(STDOUT_FILENO) != 0;
|
||||
}
|
||||
|
||||
std::size_t Progress::terminal_width() {
|
||||
struct winsize ws{};
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||
return ws.ws_col;
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
std::string Progress::format_count(std::size_t n) {
|
||||
if (n >= 1'000'000'000) {
|
||||
return std::format("{:.1f}B", static_cast<double>(n) / 1'000'000'000.0);
|
||||
}
|
||||
if (n >= 1'000'000) {
|
||||
return std::format("{:.1f}M", static_cast<double>(n) / 1'000'000.0);
|
||||
}
|
||||
if (n >= 1'000) {
|
||||
return std::format("{:.1f}K", static_cast<double>(n) / 1'000.0);
|
||||
}
|
||||
return std::format("{}", n);
|
||||
}
|
||||
|
||||
std::string Progress::format_time(double seconds) {
|
||||
auto mins = static_cast<int>(seconds) / 60;
|
||||
auto secs = seconds - static_cast<double>(mins * 60);
|
||||
return std::format("{:02d}:{:05.2f}", mins, secs);
|
||||
}
|
||||
|
||||
std::string Progress::format_speed(double hps) {
|
||||
return format_count(static_cast<std::size_t>(hps)) + " h/s";
|
||||
}
|
||||
|
||||
std::string Progress::render_bar(double fraction, std::size_t width) const {
|
||||
if (width < config::PROGRESS_BAR_MIN_WIDTH) {
|
||||
width = config::PROGRESS_BAR_MIN_WIDTH;
|
||||
}
|
||||
|
||||
auto filled = static_cast<std::size_t>(fraction * static_cast<double>(width));
|
||||
if (filled > width) { filled = width; }
|
||||
|
||||
std::string bar;
|
||||
bar += config::box::BAR_LEFT;
|
||||
for (std::size_t i = 0; i < width; ++i) {
|
||||
bar += (i < filled) ? config::box::BLOCK_FULL : config::box::BLOCK_EMPTY;
|
||||
}
|
||||
bar += config::box::BAR_RIGHT;
|
||||
return bar;
|
||||
}
|
||||
|
||||
void Progress::print_banner() const {
|
||||
if (!is_tty()) { return; }
|
||||
|
||||
auto w = terminal_width();
|
||||
auto inner_width = (w > 6) ? w - 6 : 40;
|
||||
|
||||
std::string top_border(inner_width, '\0');
|
||||
std::string bot_border(inner_width, '\0');
|
||||
top_border.clear();
|
||||
bot_border.clear();
|
||||
for (std::size_t i = 0; i < inner_width; ++i) {
|
||||
top_border += config::box::HORIZONTAL;
|
||||
bot_border += config::box::HORIZONTAL;
|
||||
}
|
||||
|
||||
auto line1 = std::format(" {} {} v{}",
|
||||
config::APP_NAME, config::box::VERTICAL, config::VERSION);
|
||||
auto line2 = std::format(" {} {} {} {} {} threads",
|
||||
config::box::VERTICAL, algorithm_, config::box::VERTICAL,
|
||||
attack_mode_, thread_count_);
|
||||
|
||||
std::println("{}{}{}{}{}",
|
||||
config::color::CYAN, config::box::TOP_LEFT,
|
||||
top_border, config::box::TOP_RIGHT, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
|
||||
config::box::VERTICAL, line1, inner_width - 1,
|
||||
config::box::VERTICAL, config::color::RESET);
|
||||
std::println("{}{} {:<{}}{}{}", config::color::CYAN,
|
||||
config::box::VERTICAL, line2, inner_width - 1,
|
||||
config::box::VERTICAL, config::color::RESET);
|
||||
std::println("{}{}{}{}{}",
|
||||
config::color::CYAN, config::box::BOTTOM_LEFT,
|
||||
bot_border, config::box::BOTTOM_RIGHT, config::color::RESET);
|
||||
std::println("");
|
||||
}
|
||||
|
||||
void Progress::update() {
|
||||
if (!is_tty()) { return; }
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double elapsed = std::chrono::duration<double>(now - start_time).count();
|
||||
auto tested_val = tested_.load(std::memory_order_relaxed);
|
||||
|
||||
double fraction = (total_ > 0)
|
||||
? static_cast<double>(tested_val) / static_cast<double>(total_)
|
||||
: 0.0;
|
||||
if (fraction > 1.0) { fraction = 1.0; }
|
||||
|
||||
double speed = (elapsed > 0.0)
|
||||
? static_cast<double>(tested_val) / elapsed
|
||||
: 0.0;
|
||||
|
||||
double eta = (speed > 0.0 && total_ > tested_val)
|
||||
? static_cast<double>(total_ - tested_val) / speed
|
||||
: 0.0;
|
||||
|
||||
auto bar_width = terminal_width();
|
||||
bar_width = (bar_width > 30) ? bar_width - 20 : 10;
|
||||
|
||||
std::print("\033[3A");
|
||||
|
||||
std::println(" {}{} {:.1f}%{}",
|
||||
config::color::YELLOW, render_bar(fraction, bar_width),
|
||||
fraction * 100.0, config::color::RESET);
|
||||
std::println(" {} {} {} {} {} {} {} ~{}{}",
|
||||
config::icon::BOLT, config::color::CYAN,
|
||||
format_speed(speed), config::color::RESET,
|
||||
config::icon::TIMER, config::color::CYAN,
|
||||
format_time(elapsed), format_time(eta),
|
||||
config::color::RESET);
|
||||
std::println(" {} {} {} / {} candidates{}",
|
||||
config::icon::CHART, config::color::CYAN,
|
||||
format_count(tested_val), format_count(total_),
|
||||
config::color::RESET);
|
||||
}
|
||||
|
||||
void Progress::print_cracked(const CrackResult& result) const {
|
||||
if (!is_tty()) {
|
||||
std::println("{}", result.plaintext);
|
||||
return;
|
||||
}
|
||||
|
||||
std::print("\033[3A\033[J");
|
||||
|
||||
std::println(" {}{} CRACKED {}{}{}",
|
||||
config::color::GREEN, config::icon::CHECK,
|
||||
std::string(30, '-'), config::color::RESET, "");
|
||||
std::println(" {}Password: {}{}{}",
|
||||
config::color::BOLD, config::color::GREEN,
|
||||
result.plaintext, config::color::RESET);
|
||||
std::println(" Hash: {}", result.hash);
|
||||
std::println(" Algorithm: {}", result.algorithm);
|
||||
std::println(" Time: {} {} {}",
|
||||
format_time(result.elapsed_seconds),
|
||||
config::box::VERTICAL,
|
||||
format_speed(result.hashes_per_second));
|
||||
}
|
||||
|
||||
void Progress::print_exhausted(std::string_view hash,
|
||||
std::string_view algorithm) const {
|
||||
if (!is_tty()) {
|
||||
std::println("NOT FOUND");
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
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");
|
||||
|
||||
std::println(" {}{} EXHAUSTED {}{}{}",
|
||||
config::color::RED, config::icon::CROSS,
|
||||
std::string(30, '-'), config::color::RESET, "");
|
||||
std::println(" Hash: {}", hash);
|
||||
std::println(" Algorithm: {}", algorithm);
|
||||
std::println(" Tested: {} candidates in {}",
|
||||
format_count(tested_val), format_time(elapsed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ ThreadPool::ThreadPool(unsigned thread_count)
|
|||
: thread_count_(thread_count > 0 ? thread_count
|
||||
: std::thread::hardware_concurrency()) {}
|
||||
|
||||
void ThreadPool::run(WorkFn) {}
|
||||
void ThreadPool::run(WorkFn work) {
|
||||
std::vector<std::jthread> threads;
|
||||
threads.reserve(thread_count_);
|
||||
|
||||
for (unsigned i = 0; i < thread_count_; ++i) {
|
||||
threads.emplace_back([this, &work, i] {
|
||||
work(i, thread_count_, state_);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SharedState& ThreadPool::state() { return state_; }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,44 @@
|
|||
// test_engine.cpp
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/core/Engine.hpp"
|
||||
#include "src/hash/SHA256Hasher.hpp"
|
||||
|
||||
TEST(EngineTest, Stub) {
|
||||
EXPECT_TRUE(true);
|
||||
TEST(EngineTest, CracksSHA256WithDictionary) {
|
||||
CrackConfig cfg;
|
||||
cfg.target_hash =
|
||||
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8";
|
||||
cfg.wordlist_path = "tests/data/small_wordlist.txt";
|
||||
cfg.thread_count = 2;
|
||||
|
||||
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result->plaintext, "password");
|
||||
}
|
||||
|
||||
TEST(EngineTest, ReturnsExhaustedWhenNotFound) {
|
||||
CrackConfig cfg;
|
||||
cfg.target_hash = std::string(64, 'f');
|
||||
cfg.wordlist_path = "tests/data/small_wordlist.txt";
|
||||
cfg.thread_count = 1;
|
||||
|
||||
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
|
||||
EXPECT_FALSE(result.has_value());
|
||||
EXPECT_EQ(result.error(), CrackError::Exhausted);
|
||||
}
|
||||
|
||||
TEST(EngineTest, CracksWithSalt) {
|
||||
SHA256Hasher hasher;
|
||||
auto salted_hash = hasher.hash("saltpassword");
|
||||
|
||||
CrackConfig cfg;
|
||||
cfg.target_hash = salted_hash;
|
||||
cfg.wordlist_path = "tests/data/small_wordlist.txt";
|
||||
cfg.salt = "salt";
|
||||
cfg.salt_position = "prepend";
|
||||
cfg.thread_count = 1;
|
||||
|
||||
auto result = Engine::crack<SHA256Hasher, DictionaryAttack>(cfg);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(result->plaintext, "password");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue