diff --git a/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp b/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp new file mode 100644 index 00000000..b5775ebd --- /dev/null +++ b/PROJECTS/beginner/hash-cracker/src/core/Engine.hpp @@ -0,0 +1,148 @@ +// ©AngelaMos | 2026 +// Engine.hpp + +#pragma once + +#include +#include +#include +#include +#include +#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 + static auto crack(const CrackConfig& cfg) + -> std::expected; +}; + +template +auto Engine::crack(const CrackConfig& cfg) + -> std::expected { + 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) { + BruteForceAttack probe(cfg.charset, cfg.max_length, 0, 1); + return probe.total(); + } else if constexpr (std::same_as) { + 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) { + return std::expected( + BruteForceAttack(cfg.charset, cfg.max_length, tid, total)); + } else if constexpr (std::same_as) { + 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(end - start).count(); + auto tested = pool.state().tested_count.load(std::memory_order_relaxed); + double speed = (elapsed > 0.0) ? static_cast(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); +} diff --git a/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp b/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp index 4f8849f9..f225f2ca 100644 --- a/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp +++ b/PROJECTS/beginner/hash-cracker/src/display/Progress.cpp @@ -3,6 +3,12 @@ #include "src/display/Progress.hpp" #include "src/config/Config.hpp" +#include +#include +#include +#include + +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& 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(n) / 1'000'000'000.0); + } + if (n >= 1'000'000) { + return std::format("{:.1f}M", static_cast(n) / 1'000'000.0); + } + if (n >= 1'000) { + return std::format("{:.1f}K", static_cast(n) / 1'000.0); + } + return std::format("{}", n); +} + +std::string Progress::format_time(double seconds) { + auto mins = static_cast(seconds) / 60; + auto secs = seconds - static_cast(mins * 60); + return std::format("{:02d}:{:05.2f}", mins, secs); +} + +std::string Progress::format_speed(double hps) { + return format_count(static_cast(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(fraction * static_cast(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(now - start_time).count(); + auto tested_val = tested_.load(std::memory_order_relaxed); + + double fraction = (total_ > 0) + ? static_cast(tested_val) / static_cast(total_) + : 0.0; + if (fraction > 1.0) { fraction = 1.0; } + + double speed = (elapsed > 0.0) + ? static_cast(tested_val) / elapsed + : 0.0; + + double eta = (speed > 0.0 && total_ > tested_val) + ? static_cast(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(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)); +} diff --git a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp index cc993029..2f45e9d4 100644 --- a/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp +++ b/PROJECTS/beginner/hash-cracker/src/threading/ThreadPool.cpp @@ -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 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_; } diff --git a/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp b/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp index e140f7c6..5a6972ab 100644 --- a/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp +++ b/PROJECTS/beginner/hash-cracker/tests/test_engine.cpp @@ -2,7 +2,44 @@ // test_engine.cpp #include +#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(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(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(cfg); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->plaintext, "password"); }