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/display/Progress.hpp"
|
||||||
#include "src/config/Config.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,
|
Progress::Progress(std::string_view algorithm, std::string_view attack_mode,
|
||||||
unsigned thread_count, std::size_t total_candidates,
|
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)
|
const std::atomic<std::size_t>& tested)
|
||||||
: algorithm_(algorithm), attack_mode_(attack_mode),
|
: algorithm_(algorithm), attack_mode_(attack_mode),
|
||||||
thread_count_(thread_count), total_(total_candidates),
|
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 {}
|
bool Progress::is_tty() {
|
||||||
void Progress::update() {}
|
return isatty(STDOUT_FILENO) != 0;
|
||||||
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() {
|
||||||
std::size_t Progress::terminal_width() { return 80; }
|
struct winsize ws{};
|
||||||
std::string Progress::render_bar(double, std::size_t) const { return ""; }
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||||
std::string Progress::format_count(std::size_t) { return ""; }
|
return ws.ws_col;
|
||||||
std::string Progress::format_time(double) { return ""; }
|
}
|
||||||
std::string Progress::format_speed(double) { return ""; }
|
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
|
: thread_count_(thread_count > 0 ? thread_count
|
||||||
: std::thread::hardware_concurrency()) {}
|
: 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_; }
|
SharedState& ThreadPool::state() { return state_; }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,44 @@
|
||||||
// test_engine.cpp
|
// test_engine.cpp
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include "src/core/Engine.hpp"
|
||||||
|
#include "src/hash/SHA256Hasher.hpp"
|
||||||
|
|
||||||
TEST(EngineTest, Stub) {
|
TEST(EngineTest, CracksSHA256WithDictionary) {
|
||||||
EXPECT_TRUE(true);
|
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