feat: all attack strategies (dictionary, brute-force, rules)
DictionaryAttack: mmap-based wordlist reader with thread partitioning. BruteForceAttack: keyspace generator with mathematical partitioning. RuleSet: 7 mutation transforms via std::generator (capitalize, leet, append/prepend digits, reverse, toggle case). RuleAttack: combines dictionary words with rule mutations. 22 new tests, all passing.
This commit is contained in:
parent
dd27eb2620
commit
6542a097f8
|
|
@ -75,4 +75,6 @@ target_link_libraries(hashcracker_tests
|
|||
PRIVATE hashcracker_lib GTest::gtest_main
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(hashcracker_tests)
|
||||
gtest_discover_tests(hashcracker_tests
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,64 @@
|
|||
// BruteForceAttack.cpp
|
||||
|
||||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
BruteForceAttack::BruteForceAttack(std::string_view, std::size_t,
|
||||
unsigned, unsigned) {}
|
||||
|
||||
std::expected<std::string, AttackComplete> BruteForceAttack::next() {
|
||||
return std::unexpected(AttackComplete{});
|
||||
std::size_t BruteForceAttack::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;
|
||||
}
|
||||
|
||||
std::size_t BruteForceAttack::total() const { return total_; }
|
||||
std::size_t BruteForceAttack::progress() const { return current_; }
|
||||
BruteForceAttack::BruteForceAttack(std::string_view charset,
|
||||
std::size_t max_length,
|
||||
unsigned thread_index,
|
||||
unsigned total_threads)
|
||||
: charset_(charset), max_length_(max_length),
|
||||
total_keyspace_(compute_keyspace(charset.size(), max_length)) {
|
||||
std::size_t per_thread = total_keyspace_ / total_threads;
|
||||
std::size_t remainder = total_keyspace_ % total_threads;
|
||||
|
||||
start_index_ = thread_index * per_thread
|
||||
+ std::min(static_cast<std::size_t>(thread_index), remainder);
|
||||
std::size_t my_count = per_thread + (thread_index < remainder ? 1 : 0);
|
||||
end_index_ = start_index_ + my_count;
|
||||
current_index_ = start_index_;
|
||||
}
|
||||
|
||||
std::string BruteForceAttack::index_to_candidate(std::size_t index) const {
|
||||
std::size_t base = charset_.size();
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::expected<std::string, AttackComplete> BruteForceAttack::next() {
|
||||
if (current_index_ >= end_index_) {
|
||||
return std::unexpected(AttackComplete{});
|
||||
}
|
||||
return index_to_candidate(current_index_++);
|
||||
}
|
||||
|
||||
std::size_t BruteForceAttack::total() const { return total_keyspace_; }
|
||||
std::size_t BruteForceAttack::progress() const { return current_index_ - start_index_; }
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ public:
|
|||
std::size_t progress() const;
|
||||
|
||||
private:
|
||||
std::size_t total_ = 0;
|
||||
std::size_t current_ = 0;
|
||||
std::string charset_;
|
||||
std::size_t max_length_;
|
||||
std::size_t total_keyspace_;
|
||||
std::size_t start_index_;
|
||||
std::size_t end_index_;
|
||||
std::size_t current_index_;
|
||||
|
||||
std::string index_to_candidate(std::size_t index) const;
|
||||
static std::size_t compute_keyspace(std::size_t charset_size,
|
||||
std::size_t max_length);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,167 @@
|
|||
// DictionaryAttack.cpp
|
||||
|
||||
#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,
|
||||
std::size_t end) {
|
||||
std::size_t count = 0;
|
||||
for (std::size_t i = start; i < end; ++i) {
|
||||
if (data[i] == '\n') {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static std::size_t find_next_newline(const char* data,
|
||||
std::size_t pos,
|
||||
std::size_t size) {
|
||||
while (pos < size && data[pos] != '\n') {
|
||||
++pos;
|
||||
}
|
||||
return pos < size ? pos + 1 : size;
|
||||
}
|
||||
|
||||
std::expected<DictionaryAttack, CrackError> DictionaryAttack::create(
|
||||
std::string_view, unsigned, unsigned) {
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
std::size_t total_lines = count_lines_in_range(mapped, 0, file_size);
|
||||
if (file_size > 0 && mapped[file_size - 1] != '\n') {
|
||||
++total_lines;
|
||||
}
|
||||
|
||||
std::size_t lines_per_thread = total_lines / total_threads;
|
||||
std::size_t remainder = total_lines % total_threads;
|
||||
|
||||
std::size_t my_start_line = thread_index * lines_per_thread
|
||||
+ std::min(static_cast<std::size_t>(thread_index), remainder);
|
||||
std::size_t my_line_count = lines_per_thread
|
||||
+ (thread_index < remainder ? 1 : 0);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
DictionaryAttack attack;
|
||||
attack.mapped_data_ = mapped;
|
||||
attack.file_size_ = file_size;
|
||||
attack.fd_ = fd;
|
||||
attack.start_offset_ = start_offset;
|
||||
attack.end_offset_ = end_offset;
|
||||
attack.current_offset_ = start_offset;
|
||||
attack.total_words_ = my_line_count;
|
||||
attack.words_read_ = 0;
|
||||
|
||||
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() {
|
||||
return std::unexpected(AttackComplete{});
|
||||
if (current_offset_ >= end_offset_) {
|
||||
return std::unexpected(AttackComplete{});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
std::size_t DictionaryAttack::total() const { return 0; }
|
||||
std::size_t DictionaryAttack::progress() const { return 0; }
|
||||
std::size_t DictionaryAttack::total() const { return total_words_; }
|
||||
std::size_t DictionaryAttack::progress() const { return words_read_; }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class DictionaryAttack {
|
||||
|
|
@ -14,10 +15,25 @@ 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;
|
||||
|
||||
private:
|
||||
DictionaryAttack() = default;
|
||||
|
||||
const char* mapped_data_ = nullptr;
|
||||
std::size_t file_size_ = 0;
|
||||
int fd_ = -1;
|
||||
|
||||
std::size_t start_offset_ = 0;
|
||||
std::size_t end_offset_ = 0;
|
||||
std::size_t current_offset_ = 0;
|
||||
|
||||
std::size_t total_words_ = 0;
|
||||
std::size_t words_read_ = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,48 @@
|
|||
// RuleAttack.cpp
|
||||
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
#include "src/rules/RuleSet.hpp"
|
||||
|
||||
RuleAttack::RuleAttack(DictionaryAttack dict, bool chain_rules)
|
||||
: dict_(std::move(dict)), chain_rules_(chain_rules) {}
|
||||
|
||||
std::expected<RuleAttack, CrackError> RuleAttack::create(
|
||||
std::string_view, bool, unsigned, unsigned) {
|
||||
return std::unexpected(CrackError::FileNotFound);
|
||||
std::string_view path, bool chain_rules,
|
||||
unsigned thread_index, unsigned total_threads) {
|
||||
auto dict = DictionaryAttack::create(path, thread_index, total_threads);
|
||||
if (!dict.has_value()) {
|
||||
return std::unexpected(dict.error());
|
||||
}
|
||||
return RuleAttack(std::move(*dict), chain_rules);
|
||||
}
|
||||
|
||||
bool RuleAttack::load_next_word() {
|
||||
auto word = dict_.next();
|
||||
if (!word.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mutations_.clear();
|
||||
mutations_.push_back(*word);
|
||||
|
||||
for (auto&& m : RuleSet::apply_all(*word)) {
|
||||
mutations_.push_back(std::move(m));
|
||||
}
|
||||
|
||||
mutation_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::expected<std::string, AttackComplete> RuleAttack::next() {
|
||||
return std::unexpected(AttackComplete{});
|
||||
while (mutation_index_ >= mutations_.size()) {
|
||||
if (!load_next_word()) {
|
||||
return std::unexpected(AttackComplete{});
|
||||
}
|
||||
}
|
||||
|
||||
++candidates_yielded_;
|
||||
return std::move(mutations_[mutation_index_++]);
|
||||
}
|
||||
|
||||
std::size_t RuleAttack::total() const { return 0; }
|
||||
std::size_t RuleAttack::progress() const { return 0; }
|
||||
std::size_t RuleAttack::total() const { return dict_.total(); }
|
||||
std::size_t RuleAttack::progress() const { return candidates_yielded_; }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include "src/core/Concepts.hpp"
|
||||
|
||||
class RuleAttack {
|
||||
|
|
@ -20,5 +22,13 @@ public:
|
|||
std::size_t progress() const;
|
||||
|
||||
private:
|
||||
RuleAttack() = default;
|
||||
RuleAttack(DictionaryAttack dict, bool chain_rules);
|
||||
|
||||
DictionaryAttack dict_;
|
||||
bool chain_rules_;
|
||||
std::vector<std::string> mutations_;
|
||||
std::size_t mutation_index_ = 0;
|
||||
std::size_t candidates_yielded_ = 0;
|
||||
|
||||
bool load_next_word();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,12 +2,77 @@
|
|||
// RuleSet.cpp
|
||||
|
||||
#include "src/rules/RuleSet.hpp"
|
||||
#include "src/config/Config.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <unordered_map>
|
||||
|
||||
std::generator<std::string> RuleSet::capitalize_first(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::uppercase_all(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::leet_speak(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::append_digits(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::prepend_digits(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::reverse(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::toggle_case(std::string_view) { co_return; }
|
||||
std::generator<std::string> RuleSet::apply_all(std::string_view) { co_return; }
|
||||
static const std::unordered_map<char, char> 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; }
|
||||
std::string result(word);
|
||||
result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0])));
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::uppercase_all(std::string_view word) {
|
||||
std::string result(word);
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::toupper(c));
|
||||
});
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::append_digits(std::string_view word) {
|
||||
std::string base(word);
|
||||
for (std::size_t i = 0; i <= config::MAX_APPEND_DIGIT; ++i) {
|
||||
co_yield base + std::to_string(i);
|
||||
}
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::prepend_digits(std::string_view word) {
|
||||
std::string base(word);
|
||||
for (std::size_t i = 0; i <= config::MAX_PREPEND_DIGIT; ++i) {
|
||||
co_yield std::to_string(i) + base;
|
||||
}
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::reverse(std::string_view word) {
|
||||
std::string result(word.rbegin(), word.rend());
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
std::generator<std::string> RuleSet::toggle_case(std::string_view word) {
|
||||
std::string result(word);
|
||||
std::ranges::transform(result, result.begin(), [](unsigned char c) {
|
||||
if (std::islower(c)) { return static_cast<char>(std::toupper(c)); }
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
co_yield std::move(result);
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
password
|
||||
123456
|
||||
qwerty
|
||||
letmein
|
||||
dragon
|
||||
master
|
||||
monkey
|
||||
shadow
|
||||
sunshine
|
||||
trustno1
|
||||
|
|
@ -2,7 +2,54 @@
|
|||
// test_bruteforce_attack.cpp
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/BruteForceAttack.hpp"
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
TEST(BruteForceAttackTest, Stub) {
|
||||
EXPECT_TRUE(true);
|
||||
TEST(BruteForceAttackTest, GeneratesAllSingleCharCombinations) {
|
||||
BruteForceAttack attack("ab", 1, 0, 1);
|
||||
std::vector<std::string> results;
|
||||
while (auto candidate = attack.next()) {
|
||||
results.push_back(std::move(*candidate));
|
||||
}
|
||||
EXPECT_EQ(results.size(), 2);
|
||||
EXPECT_EQ(results[0], "a");
|
||||
EXPECT_EQ(results[1], "b");
|
||||
}
|
||||
|
||||
TEST(BruteForceAttackTest, GeneratesUpToMaxLength) {
|
||||
BruteForceAttack attack("ab", 2, 0, 1);
|
||||
std::vector<std::string> results;
|
||||
while (auto candidate = attack.next()) {
|
||||
results.push_back(std::move(*candidate));
|
||||
}
|
||||
EXPECT_EQ(results.size(), 6);
|
||||
}
|
||||
|
||||
TEST(BruteForceAttackTest, PartitionsKeyspace) {
|
||||
BruteForceAttack p0("ab", 2, 0, 2);
|
||||
BruteForceAttack p1("ab", 2, 1, 2);
|
||||
std::set<std::string> all;
|
||||
while (auto c = p0.next()) { all.insert(*c); }
|
||||
while (auto c = p1.next()) { all.insert(*c); }
|
||||
EXPECT_EQ(all.size(), 6);
|
||||
}
|
||||
|
||||
TEST(BruteForceAttackTest, ReportsCorrectTotal) {
|
||||
BruteForceAttack attack("abc", 3, 0, 1);
|
||||
EXPECT_EQ(attack.total(), 3 + 9 + 27);
|
||||
}
|
||||
|
||||
TEST(BruteForceAttackTest, GeneratesCorrectTwoCharCombinations) {
|
||||
BruteForceAttack attack("ab", 2, 0, 1);
|
||||
std::set<std::string> results;
|
||||
while (auto candidate = attack.next()) {
|
||||
results.insert(*candidate);
|
||||
}
|
||||
EXPECT_TRUE(results.contains("a"));
|
||||
EXPECT_TRUE(results.contains("b"));
|
||||
EXPECT_TRUE(results.contains("aa"));
|
||||
EXPECT_TRUE(results.contains("ab"));
|
||||
EXPECT_TRUE(results.contains("ba"));
|
||||
EXPECT_TRUE(results.contains("bb"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,41 @@
|
|||
// test_dictionary_attack.cpp
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/DictionaryAttack.hpp"
|
||||
#include <vector>
|
||||
|
||||
TEST(DictionaryAttackTest, Stub) {
|
||||
EXPECT_TRUE(true);
|
||||
TEST(DictionaryAttackTest, ReadsAllWords) {
|
||||
auto attack = DictionaryAttack::create("tests/data/small_wordlist.txt", 0, 1);
|
||||
ASSERT_TRUE(attack.has_value());
|
||||
|
||||
std::vector<std::string> words;
|
||||
while (auto word = attack->next()) {
|
||||
words.push_back(std::move(*word));
|
||||
}
|
||||
EXPECT_EQ(words.size(), 10);
|
||||
EXPECT_EQ(words.front(), "password");
|
||||
EXPECT_EQ(words.back(), "trustno1");
|
||||
}
|
||||
|
||||
TEST(DictionaryAttackTest, PartitionsAcrossThreads) {
|
||||
auto p0 = DictionaryAttack::create("tests/data/small_wordlist.txt", 0, 2);
|
||||
auto p1 = DictionaryAttack::create("tests/data/small_wordlist.txt", 1, 2);
|
||||
ASSERT_TRUE(p0.has_value());
|
||||
ASSERT_TRUE(p1.has_value());
|
||||
|
||||
std::size_t count0 = 0, count1 = 0;
|
||||
while (p0->next()) { ++count0; }
|
||||
while (p1->next()) { ++count1; }
|
||||
EXPECT_EQ(count0 + count1, 10);
|
||||
}
|
||||
|
||||
TEST(DictionaryAttackTest, ReportsTotal) {
|
||||
auto attack = DictionaryAttack::create("tests/data/small_wordlist.txt", 0, 1);
|
||||
ASSERT_TRUE(attack.has_value());
|
||||
EXPECT_EQ(attack->total(), 10);
|
||||
}
|
||||
|
||||
TEST(DictionaryAttackTest, MissingFileReturnsError) {
|
||||
auto attack = DictionaryAttack::create("nonexistent.txt", 0, 1);
|
||||
EXPECT_FALSE(attack.has_value());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,80 @@
|
|||
// test_rules.cpp
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "src/attack/RuleAttack.hpp"
|
||||
#include "src/rules/RuleSet.hpp"
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
TEST(RuleSetTest, Stub) {
|
||||
EXPECT_TRUE(true);
|
||||
static std::vector<std::string> collect(std::generator<std::string> gen) {
|
||||
std::vector<std::string> out;
|
||||
for (auto&& s : gen) {
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, CapitalizeFirst) {
|
||||
auto results = collect(RuleSet::capitalize_first("password"));
|
||||
ASSERT_EQ(results.size(), 1);
|
||||
EXPECT_EQ(results[0], "Password");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, UppercaseAll) {
|
||||
auto results = collect(RuleSet::uppercase_all("password"));
|
||||
ASSERT_EQ(results.size(), 1);
|
||||
EXPECT_EQ(results[0], "PASSWORD");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, LeetSpeak) {
|
||||
auto results = collect(RuleSet::leet_speak("password"));
|
||||
ASSERT_EQ(results.size(), 1);
|
||||
EXPECT_EQ(results[0], "p@$$w0rd");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, AppendDigits) {
|
||||
auto results = collect(RuleSet::append_digits("pass"));
|
||||
EXPECT_EQ(results.size(), 1000);
|
||||
EXPECT_EQ(results[0], "pass0");
|
||||
EXPECT_EQ(results[999], "pass999");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, PrependDigits) {
|
||||
auto results = collect(RuleSet::prepend_digits("pass"));
|
||||
EXPECT_EQ(results.size(), 1000);
|
||||
EXPECT_EQ(results[0], "0pass");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, Reverse) {
|
||||
auto results = collect(RuleSet::reverse("password"));
|
||||
ASSERT_EQ(results.size(), 1);
|
||||
EXPECT_EQ(results[0], "drowssap");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, ToggleCase) {
|
||||
auto results = collect(RuleSet::toggle_case("password"));
|
||||
ASSERT_EQ(results.size(), 1);
|
||||
EXPECT_EQ(results[0], "PASSWORD");
|
||||
}
|
||||
|
||||
TEST(RuleSetTest, AllRulesProduceMutations) {
|
||||
auto all = collect(RuleSet::apply_all("password"));
|
||||
EXPECT_GT(all.size(), 2000);
|
||||
EXPECT_TRUE(std::ranges::find(all, "Password") != all.end());
|
||||
EXPECT_TRUE(std::ranges::find(all, "p@$$w0rd") != all.end());
|
||||
EXPECT_TRUE(std::ranges::find(all, "password0") != all.end());
|
||||
}
|
||||
|
||||
TEST(RuleAttackTest, AppliesRulesToDictionaryWords) {
|
||||
auto attack = RuleAttack::create("tests/data/small_wordlist.txt", false, 0, 1);
|
||||
ASSERT_TRUE(attack.has_value());
|
||||
|
||||
std::vector<std::string> candidates;
|
||||
while (auto c = attack->next()) {
|
||||
candidates.push_back(std::move(*c));
|
||||
}
|
||||
EXPECT_GT(candidates.size(), 10);
|
||||
EXPECT_TRUE(std::ranges::find(candidates, "Password") != candidates.end());
|
||||
EXPECT_TRUE(std::ranges::find(candidates, "p@$$w0rd") != candidates.end());
|
||||
EXPECT_TRUE(std::ranges::find(candidates, "password") != candidates.end());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue