diff --git a/include/password.hpp b/include/password.hpp index 92d2a71..bd6dbee 100644 --- a/include/password.hpp +++ b/include/password.hpp @@ -24,4 +24,7 @@ auto recoverPassword(const Keys& keys, const std::vector& charset, std::size_t maxLength, std::string& start, int jobs, bool exhaustive, Progress& progress) -> std::vector; +auto recoverPassword(const Keys& keys, const std::vector>& mask, std::string& start, int jobs, + bool exhaustive, Progress& progress) -> std::vector; + #endif // BKCRACK_PASSWORD_HPP diff --git a/src/main.cpp b/src/main.cpp index 37d80ea..3af79a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -311,7 +311,7 @@ try } // recover password - if (args.bruteforce) + if (args.bruteforce || args.mask) { std::cout << "[" << put_time << "] Recovering password" << std::endl; @@ -319,13 +319,22 @@ try const auto [state, restart] = [&]() -> std::pair { - const auto& charset = *args.bruteforce; - const auto& [minLength, maxLength] = args.length.value_or(Arguments::LengthInterval{}); - auto start = args.recoveryStart; - auto progress = ConsoleProgress{std::cout}; - const auto sigintHandler = SigintHandler{progress.state}; - passwords = recoverPassword(keysvec.front(), charset, minLength, maxLength, start, args.jobs, - args.exhaustive, progress); + auto start = args.recoveryStart; + auto progress = ConsoleProgress{std::cout}; + const auto sigintHandler = SigintHandler{progress.state}; + + if (args.bruteforce) + { + const auto& charset = *args.bruteforce; + const auto& [minLength, maxLength] = args.length.value_or(Arguments::LengthInterval{}); + passwords = recoverPassword(keysvec.front(), charset, minLength, maxLength, start, args.jobs, + args.exhaustive, progress); + } + else + { + passwords = recoverPassword(keysvec.front(), *args.mask, start, args.jobs, args.exhaustive, progress); + } + return {progress.state, start}; }(); diff --git a/src/password.cpp b/src/password.cpp index c7ccd45..50c411f 100644 --- a/src/password.cpp +++ b/src/password.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include template @@ -522,3 +523,452 @@ auto recoverPassword(const Keys& keys, const std::vector& charset, return solutions; } + +class MaskRecovery : public SixCharactersRecovery +{ +public: + MaskRecovery(const Keys& keys, const std::vector>& mask, + std::vector& solutions, std::mutex& solutionsMutex, bool exhaustive, Progress& progress) + : target{keys} + , mask{mask} + , solutions{solutions} + , solutionsMutex{solutionsMutex} + , exhaustive{exhaustive} + , progress{progress} + { + } + + void search(const std::string& start, std::string& restart, int jobs) + { + decisions.clear(); + + if (getSuffixSize() == 0) + setTarget(target, mask[factorIndex + 4], mask[factorIndex + 5]); + + if (parallelDepth == -1) + searchLongRecursive(Keys{}, target); + else + { + if (progressDepth) + { + auto product = int{1}; + for (auto i = 0; i < progressDepth; ++i) + product *= getCharsetAtDepth(i).size(); + + progress.done = 0; + progress.total = product; + } + searchLongParallelRecursive(Keys{}, target, start, restart, jobs); + } + } + + void onSolutionFound() + { + auto password = std::string{}; + password.append(decisions.begin() + getSuffixSize(), decisions.end()); + password.append(p.begin(), p.end()); + password.append(decisions.rbegin() + factorIndex, decisions.rend()); + + const auto isInSearchSpace = + std::all_of(p.begin(), p.end(), + [this, i = factorIndex](char c) mutable + { + const auto& charset = mask[i++]; + return std::binary_search(charset.begin(), charset.end(), static_cast(c)); + }); + + if (!isInSearchSpace) + { + progress.log( + [&password](std::ostream& os) + { + const auto flagsBefore = os.setf(std::ios::hex, std::ios::basefield); + const auto fillBefore = os.fill('0'); + + os << "Password: " << password << " (as bytes:"; + for (const auto c : password) + os << ' ' << std::setw(2) << static_cast(c); + os << ')' << std::endl; + + os.fill(fillBefore); + os.flags(flagsBefore); + + os << "Some characters do not match the given mask. Continuing." << std::endl; + }); + + return; + } + + { + const auto lock = std::scoped_lock{solutionsMutex}; + solutions.push_back(password); + } + + progress.log([&password](std::ostream& os) { os << "Password: " << password << std::endl; }); + + if (!exhaustive) + progress.state = Progress::State::EarlyExit; + } + +private: + void searchLongRecursive(const Keys& afterPrefix, const Keys& beforeSuffix) + { + const auto depth = decisions.size(); + + if (depth + 7 == mask.size()) // there is only one more character to bruteforce + { + if (factorIndex) + { + // check compatible Z{-1}[24, 32) + if (!zm1_24_32[afterPrefix.getZ() >> 24]) + return; + + decisions.emplace_back(); + + // precompute as much as we can about the next cipher state without knowing the password byte + const auto x0_partial = Crc32Tab::crc32(afterPrefix.getX(), 0); + const auto y0_partial = afterPrefix.getY() * MultTab::mult + 1; + const auto z0_partial = Crc32Tab::crc32(afterPrefix.getZ(), 0); + + for (const auto pi : getCharsetAtDepth(depth)) + { + // finish to update the cipher state + const auto x0 = x0_partial ^ Crc32Tab::crc32(0, pi); + const auto y0 = y0_partial + MultTab::getMult(lsb(x0)); + const auto z0 = z0_partial ^ Crc32Tab::crc32(0, msb(y0)); + + // SixCharactersRecovery::search is inlined below for performance + + // check compatible Z0[16,32) + if (!z0_16_32[z0 >> 16]) + continue; + + decisions.back() = pi; + + // initialize starting X, Y and Z values + x[0] = candidateX0 = x0; + y[0] = y0; + z[0] = z0; + + // complete Z values and derive Y[24,32) values + y[1] = Crc32Tab::getYi_24_32(z[1], z[1 - 1]); + z[1] = Crc32Tab::crc32(z[1 - 1], msb(y[1])); + y[2] = Crc32Tab::getYi_24_32(z[2], z[2 - 1]); + z[2] = Crc32Tab::crc32(z[2 - 1], msb(y[2])); + y[3] = Crc32Tab::getYi_24_32(z[3], z[3 - 1]); + z[3] = Crc32Tab::crc32(z[3 - 1], msb(y[3])); + y[4] = Crc32Tab::getYi_24_32(z[4], z[4 - 1]); + // z[4] = Crc32Tab::crc32(z[4 - 1], msb(y[4])); // this one is already known + + // recursively complete Y values and derive password + searchRecursive(5); + } + + decisions.pop_back(); + } + else + { + decisions.emplace_back(); + + for (const auto pi : getCharsetAtDepth(depth)) + { + auto beforeSuffix2 = beforeSuffix; + beforeSuffix2.updateBackwardPlaintext(pi); + if (depth + 1 == getSuffixSize()) + setTarget(beforeSuffix2, mask[factorIndex + 4], mask[factorIndex + 5]); + + decisions.back() = pi; + + SixCharactersRecovery::search(afterPrefix); + } + + decisions.pop_back(); + } + } + else // bruteforce the next character and continue recursively + { + decisions.emplace_back(); + + for (const auto pi : getCharsetAtDepth(depth)) + { + auto afterPrefix2 = afterPrefix; + auto beforeSuffix2 = beforeSuffix; + + if (depth < getSuffixSize()) + { + beforeSuffix2.updateBackwardPlaintext(pi); + if (depth + 1 == getSuffixSize()) + setTarget(beforeSuffix2, mask[factorIndex + 4], mask[factorIndex + 5]); + } + else + afterPrefix2.update(pi); + + decisions.back() = pi; + + searchLongRecursive(afterPrefix2, beforeSuffix2); + } + + decisions.pop_back(); + } + } + + void searchLongParallelRecursive(const Keys& afterPrefix, const Keys& beforeSuffix, const std::string& start, + std::string& restart, int jobs) + { + const auto depth = decisions.size(); + const auto& charset = getCharsetAtDepth(depth); + + auto index_start = 0; + if (decisions.size() < start.size()) + while (index_start < static_cast(charset.size()) && + charset[index_start] < static_cast(start[decisions.size()])) + ++index_start; + + if (static_cast(depth) == parallelDepth) // parallelize the next two decisions + { + const auto& nextCharset = getCharsetAtDepth(depth + 1); + const auto parallelSpaceSize = static_cast(charset.size() * nextCharset.size()); + + index_start *= charset.size(); + if (decisions.size() + 1 < start.size()) + { + const auto maxIndex = std::min(parallelSpaceSize, index_start + static_cast(nextCharset.size())); + while (index_start < static_cast(maxIndex) && + nextCharset[index_start % charset.size()] < + static_cast(start[decisions.size() + 1])) + ++index_start; + } + + decisions.resize(depth + 2); + + const auto reportProgress = static_cast(decisions.size()) == progressDepth; + const auto reportProgressCoarse = static_cast(decisions.size()) == progressDepth + 1; + + const auto& mask4 = mask[factorIndex + 4]; + const auto& mask5 = mask[factorIndex + 5]; + + const auto threadCount = std::clamp(jobs, 1, parallelSpaceSize); + auto threads = std::vector{}; + auto nextCandidateIndex = std::atomic{index_start}; + for (auto i = 0; i < threadCount; ++i) + threads.emplace_back( + [beforeSuffix, afterPrefix, &nextCandidateIndex, &charset, &nextCharset, &mask4, &mask5, depth, + suffixSize = getSuffixSize(), parallelSpaceSize, clone = *this, reportProgress, + reportProgressCoarse]() mutable + { + for (auto i = nextCandidateIndex++; i < parallelSpaceSize; i = nextCandidateIndex++) + { + const auto firstChoice = charset[i / nextCharset.size()]; + const auto secondChoice = nextCharset[i % nextCharset.size()]; + + clone.decisions[depth] = firstChoice; + clone.decisions[depth + 1] = secondChoice; + + auto afterPrefix2 = afterPrefix; + auto beforeSuffix2 = beforeSuffix; + + if (depth < suffixSize) + { + beforeSuffix2.updateBackwardPlaintext(firstChoice); + if (depth + 1 == suffixSize) + clone.setTarget(beforeSuffix2, mask4, mask5); + } + else + afterPrefix2.update(firstChoice); + + if (depth + 1 < suffixSize) + { + beforeSuffix2.updateBackwardPlaintext(secondChoice); + if (depth + 2 == suffixSize) + clone.setTarget(beforeSuffix2, mask4, mask5); + } + else + afterPrefix2.update(secondChoice); + + clone.searchLongRecursive(afterPrefix2, beforeSuffix2); + + if (reportProgress || (reportProgressCoarse && i % charset.size() == 0)) + clone.progress.done++; + + if (clone.progress.state != Progress::State::Normal) + break; + } + }); + for (auto& thread : threads) + thread.join(); + + decisions.resize(depth); + + if (nextCandidateIndex < parallelSpaceSize) + { + restart = std::string{decisions.begin(), decisions.end()}; + restart.push_back(charset[nextCandidateIndex / charset.size()]); + restart.push_back(charset[nextCandidateIndex % charset.size()]); + while (restart.size() < mask.size() - 6) + restart.push_back(getCharsetAtDepth(restart.size())[0]); + } + } + else // take next decisions recursively + { + decisions.emplace_back(); + + const auto reportProgress = static_cast(depth + 1) == progressDepth; + + if (static_cast(depth + 1) < progressDepth) + { + auto subSearchSize = 1; + for (auto i = static_cast(depth) + 1; i < progressDepth; ++i) + subSearchSize *= getCharsetAtDepth(i).size(); + progress.done += index_start * subSearchSize; + } + if (reportProgress) + progress.done += index_start; + + for (auto i = index_start; i < static_cast(charset.size()); i++) + { + const auto pi = charset[i]; + + auto afterPrefix2 = afterPrefix; + auto beforeSuffix2 = beforeSuffix; + if (depth < getSuffixSize()) + { + beforeSuffix2.updateBackwardPlaintext(pi); + if (depth + 1 == getSuffixSize()) + setTarget(beforeSuffix2, mask[factorIndex + 4], mask[factorIndex + 5]); + } + else + afterPrefix2.update(pi); + + decisions.back() = pi; + + if (progress.state != Progress::State::Normal) + { + restart = std::string{decisions.begin(), decisions.end()}; + while (restart.size() < mask.size() - 6) + restart.push_back(getCharsetAtDepth(restart.size())[0]); + break; + } + + searchLongParallelRecursive(afterPrefix2, beforeSuffix2, i == index_start ? start : "", restart, jobs); + + // Because the recursive call may explore only a fraction of its + // search space, check that it was run in full before counting progress. + if (!restart.empty()) + break; + + if (reportProgress) + progress.done++; + } + + decisions.pop_back(); + } + } + + const Keys target; + + const std::vector>& mask; + + const std::size_t factorIndex = [this] + { + // Split mask in three parts (prefix, 6 characters factor, suffix) that minimizes the search space. + // The search space size being the size of suffix space and prefix space combined, + // we minimize search space size by maximizing the factor space size. + + auto product = std::accumulate(mask.begin(), mask.begin() + 6, std::uint64_t{1}, + [](std::uint64_t acc, const std::vector& charset) + { return acc * charset.size(); }); + auto best = std::pair{product, std::size_t{0}}; + for (auto i = std::size_t{1}; i + 6 <= mask.size(); ++i) + { + product = product / mask[i - 1].size() * mask[i + 5].size(); + best = std::max(best, std::pair{product, i}); + } + + return best.second; + }(); + + auto getSuffixSize() const -> std::size_t + { + return mask.size() - factorIndex - 6; + } + + const std::vector& getCharsetAtDepth(int i) + { + if (static_cast(i) < getSuffixSize()) + return mask[mask.size() - 1 - i]; + else + return mask[i - getSuffixSize()]; + } + + int atomicWorkDepth = [this] + { + auto product = int{1}; + for (auto i = mask.size() - 6; 0 < i; --i) + { + product *= getCharsetAtDepth(i - 1).size(); + if (1 << 16 <= product) + return static_cast(i) - 1; + } + return 0; + }(); + + int parallelDepth = [this] + { + if (atomicWorkDepth < 2) + return -1; + + auto product = static_cast(getCharsetAtDepth(0).size() * getCharsetAtDepth(1).size()); + auto best = std::pair{product, std::size_t{0}}; + for (auto i = std::size_t{1}; i + 1 != static_cast(atomicWorkDepth); ++i) + { + product = product / getCharsetAtDepth(i - 1).size() * getCharsetAtDepth(i + 1).size(); + best = std::max(best, std::pair{product, i}); + } + + return (1 < best.first) ? static_cast(best.second) : -1; + }(); + + int progressDepth = [this] + { + if (parallelDepth < 0) + return 0; + + auto product = int{1}; + for (auto i = 0; i < parallelDepth + 2; ++i) + { + product *= getCharsetAtDepth(i).size(); + if (100 <= product) + return i + 1; + } + return 0; + }(); + + std::vector decisions{}; // sequence of choices to build reversed(suffix) + prefix + + std::vector& solutions; // shared output vector of valid passwords + std::mutex& solutionsMutex; + const bool exhaustive; + Progress& progress; +}; + +auto recoverPassword(const Keys& keys, const std::vector>& mask, + [[maybe_unused]] std::string& start, int jobs, bool exhaustive, Progress& progress) + -> std::vector +{ + if (mask.size() <= 6) + { + progress.log([](std::ostream& os) { os << "mask is too short !" << std::endl; }); + return {}; + } + + auto solutions = std::vector{}; + auto solutionsMutex = std::mutex{}; + auto restart = std::string{}; + auto worker = MaskRecovery{keys, mask, solutions, solutionsMutex, exhaustive, progress}; + + worker.search(start, restart, jobs); + + start = restart; + + return solutions; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b12552c..3a941ce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,18 @@ set_tests_properties(cli.bruteforce.small PROPERTIES PASS_REGULAR_EXPRESSION "P set_tests_properties(cli.bruteforce.medium PROPERTIES PASS_REGULAR_EXPRESSION "Password: q1w2e3r4t5\n") set_tests_properties(cli.bruteforce.long PROPERTIES PASS_REGULAR_EXPRESSION "Password: abcdefghijkl\n") +add_test(NAME cli.mask.constant COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -m Lorem\ ipsum\ dolor\ sit\ amet) +add_test(NAME cli.mask.prefix COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -s 1 ?s?l -m Lorem\ ipsum\ dolor\ ?1?1?1?1?1?1?1?1) +add_test(NAME cli.mask.suffix COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -s 1 ?s?l -m ?u?1?1?1?1\ ipsum\ dolor\ sit\ amet) +add_test(NAME cli.mask.factor COMMAND bkcrack -k c80f5189 ce16bd43 38247eb5 -s 1 ?s?l -m Lorem\ ipsum\ ?1?1?1?1?1?1?1?1?1\ amet) +set_tests_properties(cli.mask.constant PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") +set_tests_properties(cli.mask.prefix PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") +set_tests_properties(cli.mask.suffix PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") +set_tests_properties(cli.mask.factor PROPERTIES PASS_REGULAR_EXPRESSION "Password: Lorem ipsum dolor sit amet\n") + +add_test(NAME cli.mask.letters-digits COMMAND bkcrack -k 2cd417a0 37238582 7f1df897 -m ?l?l?l?l?l?l?l?l-?d?d?d?d?d) +set_tests_properties(cli.mask.letters-digits PROPERTIES PASS_REGULAR_EXPRESSION "Password: password-12345\n") + add_test(NAME cli.list COMMAND bkcrack -L secrets.zip WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/example) set_tests_properties(cli.list PROPERTIES PASS_REGULAR_EXPRESSION " Index Encryption Compression CRC32 Uncompressed Packed size Name